diff --git a/ruoyi-common/ruoyi-common-doc/pom.xml b/ruoyi-common/ruoyi-common-doc/pom.xml index c6199a17c..390d34f01 100644 --- a/ruoyi-common/ruoyi-common-doc/pom.xml +++ b/ruoyi-common/ruoyi-common-doc/pom.xml @@ -21,6 +21,11 @@ ruoyi-common-core + + cn.dev33 + sa-token-core + + org.springdoc springdoc-openapi-starter-webmvc-api diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java index 35b6ce9ea..b22d91139 100644 --- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/config/SpringDocConfig.java @@ -7,6 +7,8 @@ import io.swagger.v3.oas.models.security.SecurityRequirement; import lombok.RequiredArgsConstructor; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.doc.config.properties.SpringDocProperties; +import org.dromara.common.doc.core.enhancer.SaTokenJavadocResolver; +import org.dromara.common.doc.core.enhancer.SaTokenMetadataResolver; import org.dromara.common.doc.handler.OpenApiHandler; import org.springdoc.core.configuration.SpringDocConfiguration; import org.springdoc.core.customizers.OpenApiBuilderCustomizer; @@ -84,8 +86,9 @@ public class SpringDocConfig { SecurityService securityParser, SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils, Optional> openApiBuilderCustomisers, - Optional> serverBaseUrlCustomisers, Optional javadocProvider) { - return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider); + Optional> serverBaseUrlCustomisers, Optional javadocProvider, + SaTokenMetadataResolver saTokenMetadataResolver) { + return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider, saTokenMetadataResolver); } /** @@ -112,6 +115,14 @@ public class SpringDocConfig { }; } + /** + * 注册JavaDoc权限解析器 + */ + @Bean + public SaTokenMetadataResolver saTokenJavadocResolver() { + return new SaTokenJavadocResolver(); + } + /** * 单独使用一个类便于判断 解决springdoc路径拼接重复问题 * diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenJavadocResolver.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenJavadocResolver.java new file mode 100644 index 000000000..426f26a1e --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenJavadocResolver.java @@ -0,0 +1,200 @@ +package org.dromara.common.doc.core.enhancer; + +import cn.dev33.satoken.annotation.SaCheckLogin; +import cn.dev33.satoken.annotation.SaCheckPermission; +import cn.dev33.satoken.annotation.SaCheckRole; +import cn.dev33.satoken.annotation.SaIgnore; +import io.swagger.v3.oas.models.Operation; +import org.dromara.common.doc.core.model.SaTokenSecurityMetadata; +import org.springframework.web.method.HandlerMethod; + +import java.lang.annotation.Annotation; + +/** + * 基于JavaDoc的SaToken权限解析器 + * + * @author echo + */ +public class SaTokenJavadocResolver implements SaTokenMetadataResolver { + + public static final Class SA_CHECK_ROLE_CLASS = SaCheckRole.class; + public static final Class SA_CHECK_PERMISSION_CLASS = SaCheckPermission.class; + public static final Class SA_IGNORE_CLASS = SaIgnore.class; + public static final Class SA_CHECK_LOGIN = SaCheckLogin.class; + + /** + * 核心解析方法 + */ + @Override + public void resolve(HandlerMethod handlerMethod, Operation operation, SaTokenSecurityMetadata metadata) { + // 检查是否忽略校验 + if (isIgnore(handlerMethod)) { + metadata.setIgnore(true); + return; + } + + // 解析权限校验 + resolvePermissionCheck(handlerMethod, metadata); + + // 解析角色校验 + resolveRoleCheck(handlerMethod, metadata); + } + + /** + * 解析器优先级 + */ + @Override + public int getOrder() { + return 100; + } + + /** + * 判断是否支持当前HandlerMethod + */ + @Override + public boolean supports(HandlerMethod handlerMethod) { + return hasAnnotation(handlerMethod + .getMethodAnnotation(SA_CHECK_PERMISSION_CLASS)) || hasAnnotation(handlerMethod + .getMethodAnnotation(SA_CHECK_ROLE_CLASS)) || hasAnnotation(handlerMethod + .getMethodAnnotation(SA_IGNORE_CLASS)) || hasAnnotation(handlerMethod + .getBeanType() + .getAnnotation(SA_CHECK_PERMISSION_CLASS)) || hasAnnotation(handlerMethod + .getBeanType() + .getAnnotation(SA_CHECK_ROLE_CLASS)) || hasAnnotation(handlerMethod + .getBeanType() + .getAnnotation(SA_IGNORE_CLASS)); + } + + @Override + public String getName() { + return "SaTokenJavadocResolver"; + } + + /** + * 检查是否忽略校验 + */ + private boolean isIgnore(HandlerMethod handlerMethod) { + // 检查方法上的注解 + if (hasAnnotation(handlerMethod.getMethodAnnotation(SA_IGNORE_CLASS))) { + return true; + } + // 检查类上的注解 + return hasAnnotation(handlerMethod.getBeanType().getAnnotation(SA_IGNORE_CLASS)); + } + + /** + * 解析权限校验 + */ + private void resolvePermissionCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) { + // 获取方法上的注解 + Annotation methodAnnotation = handlerMethod + .getMethodAnnotation(SA_CHECK_PERMISSION_CLASS); + // 获取类上的注解 + Annotation classAnnotation = handlerMethod.getBeanType() + .getAnnotation(SA_CHECK_PERMISSION_CLASS); + + // 解析权限信息 + if (hasAnnotation(methodAnnotation)) { + resolvePermissionAnnotation(metadata, methodAnnotation); + } + if (hasAnnotation(classAnnotation)) { + resolvePermissionAnnotation(metadata, classAnnotation); + } + } + + /** + * 解析权限注解 + */ + private void resolvePermissionAnnotation(SaTokenSecurityMetadata metadata, Annotation annotation) { + try { + // 反射获取注解属性 + Object value = getAnnotationValue(annotation, "value"); + Object mode = getAnnotationValue(annotation, "mode"); + Object type = getAnnotationValue(annotation, "type"); + Object orRole = getAnnotationValue(annotation, "orRole"); + + String[] values = convertToStringArray(value); + String modeStr = mode != null ? mode.toString() : "AND"; + String typeStr = type != null ? type.toString() : ""; + String[] orRoles = convertToStringArray(orRole); + + metadata.addPermission(values, modeStr, typeStr, orRoles); + } catch (Exception e) { + // 忽略解析错误 + } + } + + /** + * 解析角色校验 + */ + private void resolveRoleCheck(HandlerMethod handlerMethod, SaTokenSecurityMetadata metadata) { + // 获取方法上的注解 + Annotation methodAnnotation = handlerMethod.getMethodAnnotation(SA_CHECK_ROLE_CLASS); + // 获取类上的注解 + Annotation classAnnotation = handlerMethod.getBeanType() + .getAnnotation(SA_CHECK_ROLE_CLASS); + + // 解析角色信息 + if (hasAnnotation(methodAnnotation)) { + resolveRoleAnnotation(metadata, methodAnnotation); + } + if (hasAnnotation(classAnnotation)) { + resolveRoleAnnotation(metadata, classAnnotation); + } + } + + /** + * 解析角色注解 + */ + private void resolveRoleAnnotation(SaTokenSecurityMetadata metadata, Annotation annotation) { + try { + // 反射获取注解属性 + Object value = getAnnotationValue(annotation, "value"); + Object mode = getAnnotationValue(annotation, "mode"); + Object type = getAnnotationValue(annotation, "type"); + + String[] values = convertToStringArray(value); + String modeStr = mode != null ? mode.toString() : "AND"; + String typeStr = type != null ? type.toString() : ""; + + metadata.addRole(values, modeStr, typeStr); + } catch (Exception e) { + // 忽略解析错误 + } + } + + /** + * 检查注解是否存在 + */ + private boolean hasAnnotation(Annotation annotation) { + return annotation != null; + } + + /** + * 获取注解属性值 + */ + private Object getAnnotationValue(Annotation annotation, String attributeName) { + try { + return annotation.annotationType().getMethod(attributeName).invoke(annotation); + } catch (Exception e) { + return null; + } + } + + /** + * 转换为字符串数组 + */ + private String[] convertToStringArray(Object value) { + if (value == null) { + return new String[0]; + } + if (value instanceof String[]) { + return (String[])value; + } + if (value instanceof String) { + return new String[] {(String)value}; + } + return new String[0]; + } + +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenMetadataResolver.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenMetadataResolver.java new file mode 100644 index 000000000..0b42b23c7 --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenMetadataResolver.java @@ -0,0 +1,38 @@ +package org.dromara.common.doc.core.enhancer; + +import io.swagger.v3.oas.models.Operation; +import org.dromara.common.doc.core.model.SaTokenSecurityMetadata; +import org.springframework.web.method.HandlerMethod; + +/** + * 权限元数据解析器接口 + * + * @author echo + */ +public interface SaTokenMetadataResolver { + + /** + * 解析权限元数据 + */ + void resolve(HandlerMethod handlerMethod, Operation operation, SaTokenSecurityMetadata metadata); + + /** + * 获取解析器优先级 + */ + int getOrder(); + + /** + * 判断是否支持当前HandlerMethod + */ + boolean supports(HandlerMethod handlerMethod); + + /** + * 获取解析器的名称 + * + * @return 解析器名称 + */ + default String getName() { + return this.getClass().getSimpleName(); + } + +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java new file mode 100644 index 000000000..e0782a20c --- /dev/null +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java @@ -0,0 +1,175 @@ +package org.dromara.common.doc.core.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonInclude.Include; +import lombok.Data; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 存储权限框架注解解析后的权限和角色信息 + * + * @author AprilWind + */ +@Data +@JsonInclude(Include.NON_EMPTY) +public class SaTokenSecurityMetadata { + + /** + * 权限校验信息列表(对应 @SaCheckPermission 注解) + */ + private List permissions = new ArrayList<>(); + + /** + * 角色校验信息列表(对应 @SaCheckRole 注解) + */ + private List roles = new ArrayList<>(); + + /** + * 是否忽略校验(对应 @SaIgnore 注解) + */ + private boolean ignore = false; + + /** + * 添加权限信息 + * + * @param values 权限值数组 + * @param mode 校验模式(AND/OR) + * @param type 权限类型 + * @param orRoles 或角色数组 + */ + public void addPermission(String[] values, String mode, String type, String[] orRoles) { + if (values != null && values.length > 0) { + AuthInfo authInfo = new AuthInfo(); + authInfo.setValues(values); + authInfo.setMode(mode); + authInfo.setType(type); + if (orRoles != null && orRoles.length > 0) { + authInfo.setOrValues(orRoles); + authInfo.setOrType("role"); + } + this.permissions.add(authInfo); + } + } + + /** + * 添加角色信息 + * + * @param values 角色值数组 + * @param mode 校验模式(AND/OR) + * @param type 角色类型 + */ + public void addRole(String[] values, String mode, String type) { + if (values != null && values.length > 0) { + AuthInfo authInfo = new AuthInfo(); + authInfo.setValues(values); + authInfo.setMode(mode); + authInfo.setType(type); + this.roles.add(authInfo); + } + } + + /** + * 生成 Markdown 结构的权限说明 + * + * @return Markdown 文本 + */ + public String toMarkdownString() { + StringBuilder sb = new StringBuilder(); + sb.append("

访问权限


"); + + if (ignore) { + sb.append("> **权限策略**:忽略权限检查
"); + return sb.toString(); + } + + if (!ignore && permissions.isEmpty() && roles.isEmpty()){ + sb.append("> **权限策略**:需要登录

"); + return sb.toString(); + } + + if (!permissions.isEmpty()) { + sb.append("**权限校验:**

"); + + permissions.forEach(p -> { + String permTags = Arrays.stream(p.getValues()) + .map(v -> "`" + v + "`") + .collect(Collectors.joining(p.getModeSymbol())); + + sb.append("- ").append(permTags).append("
"); + + if (p.getOrValues() != null && p.getOrValues().length > 0) { + String orTags = Arrays.stream(p.getOrValues()) + .map(v -> "`" + v + "`") + .collect(Collectors.joining(p.getModeSymbol())); + sb.append(" - 或角色:").append(orTags).append("
"); + } + }); + + sb.append("
"); + } + + if (!roles.isEmpty()) { + sb.append("**角色校验:**

"); + + roles.forEach(r -> { + + String roleTags = Arrays.stream(r.getValues()) + .map(v -> "`" + v + "`") + .collect(Collectors.joining(r.getModeSymbol())); + + sb.append("- ").append(roleTags).append("
"); + }); + } + + return sb.toString().trim(); + } + + /** + * 认证信息 + */ + @Data + @JsonInclude(Include.NON_EMPTY) + public static class AuthInfo { + + /** + * 权限或角色值数组 + */ + private String[] values; + + /** + * 校验模式(AND/OR) + */ + private String mode; + + /** + * 类型说明 + */ + private String type; + + /** + * 或权限/角色值数组(用于权限校验时的或角色校验) + */ + private String[] orValues; + + /** + * 或值的类型(role/permission) + */ + private String orType; + + /** + * 重写mode的获取方法,返回符号而非文字 + * @return AND→&,OR→|,默认→& + */ + public String getModeSymbol() { + if (mode == null) { + return " & "; // 默认AND,返回& + } + return "AND".equalsIgnoreCase(mode) ? " & " : " | "; + } + + } +} diff --git a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java index 56b73694d..830665942 100644 --- a/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java +++ b/ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/handler/OpenApiHandler.java @@ -12,6 +12,8 @@ import io.swagger.v3.oas.models.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.dromara.common.core.utils.StreamUtils; +import org.dromara.common.doc.core.enhancer.SaTokenMetadataResolver; +import org.dromara.common.doc.core.model.SaTokenSecurityMetadata; import org.springdoc.core.customizers.OpenApiBuilderCustomizer; import org.springdoc.core.customizers.ServerBaseUrlCustomizer; import org.springdoc.core.properties.SpringDocConfigProperties; @@ -83,6 +85,11 @@ public class OpenApiHandler extends OpenAPIService { */ private final PropertyResolverUtils propertyResolverUtils; + /** + * 权限元数据解析器接口 + */ + private final SaTokenMetadataResolver saTokenJavadocResolver; + /** * The javadoc provider. */ @@ -123,7 +130,8 @@ public class OpenApiHandler extends OpenAPIService { SpringDocConfigProperties springDocConfigProperties, PropertyResolverUtils propertyResolverUtils, Optional> openApiBuilderCustomizers, Optional> serverBaseUrlCustomizers, - Optional javadocProvider) { + Optional javadocProvider, + SaTokenMetadataResolver saTokenJavadocResolver) { super(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomizers, serverBaseUrlCustomizers, javadocProvider); if (openAPI.isPresent()) { this.openAPI = openAPI.get(); @@ -140,6 +148,7 @@ public class OpenApiHandler extends OpenAPIService { this.openApiBuilderCustomisers = openApiBuilderCustomizers; this.serverBaseUrlCustomizers = serverBaseUrlCustomizers; this.javadocProvider = javadocProvider; + this.saTokenJavadocResolver = saTokenJavadocResolver; if (springDocConfigProperties.isUseFqn()) TypeNameResolver.std.setUseFqn(true); } @@ -219,7 +228,21 @@ public class OpenApiHandler extends OpenAPIService { else securityParser.buildSecurityRequirement(securityRequirements, operation); } - + String description = javadocProvider.get().getMethodJavadocDescription(handlerMethod.getMethod()); + String summary = javadocProvider.get().getFirstSentence(description); + if (StringUtils.isNotBlank(description)){ + operation.setSummary(summary); + } + // 调用SaToken解析器提取JavaDoc中的权限信息 + if (saTokenJavadocResolver.supports(handlerMethod)) { + SaTokenSecurityMetadata metadata = new SaTokenSecurityMetadata(); + saTokenJavadocResolver.resolve(handlerMethod, operation, metadata); + String markdownString = metadata.toMarkdownString(); + if (StringUtils.isNotBlank(markdownString)) { + description = description + markdownString; + } + } + operation.setDescription(description); return operation; } diff --git a/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java new file mode 100644 index 000000000..c9dada111 --- /dev/null +++ b/ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java @@ -0,0 +1,198 @@ +package org.dromara.demo.controller; + +import cn.dev33.satoken.annotation.*; +import lombok.extern.slf4j.Slf4j; +import org.dromara.common.core.domain.R; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * SaToken 权限测试 + * + * @author AprilWind + */ +@Slf4j +@RestController +@RequestMapping("/demo/SaToken") +public class SaTokenTestController { + + // ====================== 基础场景:单一校验规则 ====================== + + /** + * 场景1:仅登录校验(无角色/权限限制,只需登录态) + */ + @SaCheckLogin + @GetMapping("/basic/loginOnly") + public R loginOnly() { + log.info("【场景1】仅登录校验通过"); + return R.ok("仅登录校验通过,无需角色/权限"); + } + + /** + * 场景2:单一角色校验(AND模式,默认) + */ + @SaCheckRole("admin") + @GetMapping("/basic/singleRole") + public R singleRole() { + log.info("【场景2】单一角色(admin)校验通过"); + return R.ok("拥有admin角色,校验通过"); + } + + /** + * 场景3:单一权限校验(AND模式,默认) + */ + @SaCheckPermission("system:user:view") + @GetMapping("/basic/singlePermission") + public R singlePermission() { + log.info("【场景3】单一权限(system:user:view)校验通过"); + return R.ok("拥有system:user:view权限,校验通过"); + } + + /** + * 场景4:忽略所有权限校验(SaIgnore优先级最高) + */ + @SaIgnore + @SaCheckRole("none_exist") // 该注解会被忽略 + @GetMapping("/basic/ignoreAll") + public R ignoreAll() { + log.info("【场景4】SaIgnore忽略所有权限校验"); + return R.ok("SaIgnore生效,所有权限校验被忽略"); + } + + // ====================== 进阶场景:多条件组合(AND/OR) ====================== + + /** + * 场景5:多角色AND模式(必须同时拥有所有角色) + */ + @SaCheckRole(value = {"admin", "operator"}, mode = SaMode.AND) + @GetMapping("/advance/multiRoleAnd") + public R multiRoleAnd() { + log.info("【场景5】多角色AND模式(admin+operator)校验通过"); + return R.ok("同时拥有admin和operator角色,校验通过"); + } + + /** + * 场景6:多角色OR模式(拥有任一角色即可) + */ + @SaCheckRole(value = {"admin", "test"}, mode = SaMode.OR) + @GetMapping("/advance/multiRoleOr") + public R multiRoleOr() { + log.info("【场景6】多角色OR模式(admin|test)校验通过"); + return R.ok("拥有admin或test角色,校验通过"); + } + + /** + * 场景7:多权限AND模式(必须同时拥有所有权限) + */ + @SaCheckPermission(value = {"system:user:edit", "system:log:view"}, mode = SaMode.AND) + @GetMapping("/advance/multiPermAnd") + public R multiPermAnd() { + log.info("【场景7】多权限AND模式(system:user:edit+system:log:view)校验通过"); + return R.ok("同时拥有system:user:edit和system:log:view权限,校验通过"); + } + + /** + * 场景8:多权限OR模式(拥有任一权限即可) + */ + @SaCheckPermission(value = {"system:user:add", "system:user:delete"}, mode = SaMode.OR) + @GetMapping("/advance/multiPermOr") + public R multiPermOr() { + log.info("【场景8】多权限OR模式(system:user:add|system:user:delete)校验通过"); + return R.ok("拥有system:user:add或system:user:delete权限,校验通过"); + } + + // ====================== 高级场景:通配符/混合组合 ====================== + + /** + * 场景9:权限通配符匹配(前缀匹配) + * 拥有system:user:* 即可匹配所有用户模块权限 + */ + @SaCheckPermission("system:user:*") + @GetMapping("/advanced/permWildcardPrefix") + public R permWildcardPrefix() { + log.info("【场景9】权限通配符(system:user:*)校验通过"); + return R.ok("拥有system:user:*前缀权限,校验通过"); + } + + /** + * 场景10:角色通配符匹配(前缀匹配) + * 拥有admin_* 即可匹配所有admin开头的角色 + */ + @SaCheckRole("admin_*") + @GetMapping("/advanced/roleWildcardPrefix") + public R roleWildcardPrefix() { + log.info("【场景10】角色通配符(admin_*)校验通过"); + return R.ok("拥有admin_*前缀角色,校验通过"); + } + + /** + * 场景11:权限+角色混合AND模式(所有条件必须满足) + * 需同时满足:拥有admin角色 + 拥有system:user:all权限 + */ + @SaCheckRole("admin") + @SaCheckPermission("system:user:all") + @GetMapping("/advanced/mixRolePermAnd") + public R mixRolePermAnd() { + log.info("【场景11】角色+权限混合AND(admin+system:user:all)校验通过"); + return R.ok("拥有admin角色且拥有system:user:all权限,校验通过"); + } + + /** + * 场景12:权限+角色混合OR模式(任一条件满足即可) + * 满足任一:拥有super_admin角色 | 拥有system:manage权限 + */ + @SaCheckRole(value = {"super_admin"}, mode = SaMode.OR) + @SaCheckPermission(value = {"system:manage"}, mode = SaMode.OR) + @GetMapping("/advanced/mixRolePermOr") + public R mixRolePermOr() { + log.info("【场景12】角色+权限混合OR(super_admin|system:manage)校验通过"); + return R.ok("拥有super_admin角色或system:manage权限,校验通过"); + } + + /** + * 场景13:orRole参数(权限校验失败时,兜底角色校验) + * 核心逻辑:无system:user:export权限时,检查是否有admin/operator角色 + */ + @SaCheckPermission(value = "system:user:export", orRole = {"admin", "operator"}) + @GetMapping("/advanced/permWithOrRole") + public R permWithOrRole() { + log.info("【场景13】权限+orRole兜底校验通过"); + return R.ok("拥有system:user:export权限,或拥有admin/operator角色,校验通过"); + } + + // ====================== 特殊场景:临时权限/注解覆盖 ====================== + + /** + * 场景14:SaIgnore局部覆盖(方法注解覆盖类注解,若有) + * 假设类上有@SaCheckLogin,方法上@SaIgnore会覆盖 + */ + @SaIgnore + @GetMapping("/special/ignoreOverride") + public R ignoreOverride() { + log.info("【场景14】SaIgnore覆盖类级别权限注解"); + return R.ok("方法级SaIgnore覆盖类级别权限校验"); + } + + /** + * 场景15:临时权限校验(SaCheckPermission逻辑:临时权限>永久权限) + * 注:临时权限需通过SaToken API手动设置,如 SaHolder.getStpLogic().setTempPermission("system:temp:test") + */ + @SaCheckPermission("system:temp:test") + @GetMapping("/special/tempPermission") + public R tempPermission() { + log.info("【场景15】临时权限(system:temp:test)校验通过"); + return R.ok("临时权限校验通过(需先通过API设置临时权限)"); + } + + /** + * 场景16:登录类型指定(多端登录场景,如PC/APP/小程序) + * 注:需配合SaToken多账号体系配置 + */ + @SaCheckLogin(type = "PC") // 仅校验PC端的登录态 + @GetMapping("/special/loginTypeSpecify") + public R loginTypeSpecify() { + log.info("【场景16】指定登录类型(PC)校验通过"); + return R.ok("仅PC端登录态校验通过"); + } +}