!835 update Sa-Token 权限码展示,接口详情显示权限码,感谢nextdoc4j

* update 增强接口描述,合并JavaDoc权限信息到操作描述中
* update 优化satoken依赖引用,减少耦合性
* update 优化接口描述文本展示
* update Sa-Token 权限码展示,接口详情显示权限码,感谢nextdoc4j
This commit is contained in:
AprilWind
2026-03-05 09:06:58 +00:00
committed by 疯狂的狮子Li
parent 28772b8b30
commit 1452ae9685
7 changed files with 654 additions and 4 deletions

View File

@@ -21,6 +21,11 @@
<artifactId>ruoyi-common-core</artifactId>
</dependency>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-core</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>

View File

@@ -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<List<OpenApiBuilderCustomizer>> openApiBuilderCustomisers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> javadocProvider) {
return new OpenApiHandler(openAPI, securityParser, springDocConfigProperties, propertyResolverUtils, openApiBuilderCustomisers, serverBaseUrlCustomisers, javadocProvider);
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomisers, Optional<JavadocProvider> 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路径拼接重复问题
*

View File

@@ -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<SaCheckRole> SA_CHECK_ROLE_CLASS = SaCheckRole.class;
public static final Class<SaCheckPermission> SA_CHECK_PERMISSION_CLASS = SaCheckPermission.class;
public static final Class<SaIgnore> SA_IGNORE_CLASS = SaIgnore.class;
public static final Class<SaCheckLogin> 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];
}
}

View File

@@ -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();
}
}

View File

@@ -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<AuthInfo> permissions = new ArrayList<>();
/**
* 角色校验信息列表(对应 @SaCheckRole 注解)
*/
private List<AuthInfo> 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("<br><h3>访问权限</h3><br>");
if (ignore) {
sb.append("> **权限策略**:忽略权限检查<br>");
return sb.toString();
}
if (!ignore && permissions.isEmpty() && roles.isEmpty()){
sb.append("> **权限策略**:需要登录<br><br>");
return sb.toString();
}
if (!permissions.isEmpty()) {
sb.append("**权限校验:**<br><br>");
permissions.forEach(p -> {
String permTags = Arrays.stream(p.getValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(p.getModeSymbol()));
sb.append("- ").append(permTags).append("<br>");
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("<br>");
}
});
sb.append("<br>");
}
if (!roles.isEmpty()) {
sb.append("**角色校验:**<br><br>");
roles.forEach(r -> {
String roleTags = Arrays.stream(r.getValues())
.map(v -> "`" + v + "`")
.collect(Collectors.joining(r.getModeSymbol()));
sb.append("- ").append(roleTags).append("<br>");
});
}
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) ? " & " : " | ";
}
}
}

View File

@@ -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<List<OpenApiBuilderCustomizer>> openApiBuilderCustomizers,
Optional<List<ServerBaseUrlCustomizer>> serverBaseUrlCustomizers,
Optional<JavadocProvider> javadocProvider) {
Optional<JavadocProvider> 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;
}