From 1452ae9685e6ad12c6b4f951aa6627c61efa35a6 Mon Sep 17 00:00:00 2001
From: AprilWind <2100166581@qq.com>
Date: Thu, 5 Mar 2026 09:06:58 +0000
Subject: [PATCH] =?UTF-8?q?!835=20update=20Sa-Token=20=E6=9D=83=E9=99=90?=
=?UTF-8?q?=E7=A0=81=E5=B1=95=E7=A4=BA=EF=BC=8C=E6=8E=A5=E5=8F=A3=E8=AF=A6?=
=?UTF-8?q?=E6=83=85=E6=98=BE=E7=A4=BA=E6=9D=83=E9=99=90=E7=A0=81=EF=BC=8C?=
=?UTF-8?q?=E6=84=9F=E8=B0=A2nextdoc4j=20*=20update=20=E5=A2=9E=E5=BC=BA?=
=?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=8F=8F=E8=BF=B0=EF=BC=8C=E5=90=88=E5=B9=B6?=
=?UTF-8?q?JavaDoc=E6=9D=83=E9=99=90=E4=BF=A1=E6=81=AF=E5=88=B0=E6=93=8D?=
=?UTF-8?q?=E4=BD=9C=E6=8F=8F=E8=BF=B0=E4=B8=AD=20*=20update=20=E4=BC=98?=
=?UTF-8?q?=E5=8C=96satoken=E4=BE=9D=E8=B5=96=E5=BC=95=E7=94=A8=EF=BC=8C?=
=?UTF-8?q?=E5=87=8F=E5=B0=91=E8=80=A6=E5=90=88=E6=80=A7=20*=20update=20?=
=?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8E=A5=E5=8F=A3=E6=8F=8F=E8=BF=B0=E6=96=87?=
=?UTF-8?q?=E6=9C=AC=E5=B1=95=E7=A4=BA=20*=20update=20Sa-Token=20=E6=9D=83?=
=?UTF-8?q?=E9=99=90=E7=A0=81=E5=B1=95=E7=A4=BA=EF=BC=8C=E6=8E=A5=E5=8F=A3?=
=?UTF-8?q?=E8=AF=A6=E6=83=85=E6=98=BE=E7=A4=BA=E6=9D=83=E9=99=90=E7=A0=81?=
=?UTF-8?q?=EF=BC=8C=E6=84=9F=E8=B0=A2nextdoc4j?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
ruoyi-common/ruoyi-common-doc/pom.xml | 5 +
.../common/doc/config/SpringDocConfig.java | 15 +-
.../core/enhancer/SaTokenJavadocResolver.java | 200 ++++++++++++++++++
.../enhancer/SaTokenMetadataResolver.java | 38 ++++
.../core/model/SaTokenSecurityMetadata.java | 175 +++++++++++++++
.../common/doc/handler/OpenApiHandler.java | 27 ++-
.../controller/SaTokenTestController.java | 198 +++++++++++++++++
7 files changed, 654 insertions(+), 4 deletions(-)
create mode 100644 ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenJavadocResolver.java
create mode 100644 ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/enhancer/SaTokenMetadataResolver.java
create mode 100644 ruoyi-common/ruoyi-common-doc/src/main/java/org/dromara/common/doc/core/model/SaTokenSecurityMetadata.java
create mode 100644 ruoyi-modules/ruoyi-demo/src/main/java/org/dromara/demo/controller/SaTokenTestController.java
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端登录态校验通过");
+ }
+}