做 Java 开发的对 swagger 二次封装的 Knife4j 应该并不陌生,它是一个美化过后的在线 swagger 文档,方便开发人员以及前后端联调时候的重要利器,可能少许公司是使用类似于 apipost、apifox、postman 之类的工具进行测试,更甚至是 IDEA 中自带的 rustfulbox 之类的插件实现自测或者调试。但是为了和前后端以及测试人员同步查看接口,现公司我们采用的集成了 Knife4j 这个组件,方便快速查阅。

因为现目前在公司做业务底座脚手架的相关功能,还在一步步完善,每个接口的细粒度权限我们采用了spring security进行在接口上打注解标记是否需要哪些权限标识符,然后对应的在后台需要配置相应的权限才能访问。

由于一个前端一个页面会调用后端多个接口,前端以及测试的时候老是提示某些接口无权限又不知道是需要哪个权限,每次都需要开发人员进行告知然后再去勾选权限,为了解决这一问题,有如下方案:

1、直接提供一个大而全的接口写出接口对应的权限标识符,通过扫描 controller 上的@PreAuthorize 注解进行列出进行配置或者导入;

2、直接在 swagger 文档中描述中写出需要的权限,方便按需添加;

使用原生 swagger 的一些注解是可以实现这一功能的,但是需要额外的人工去挨个标记打特定注解,为了减少这部分工作,所以打算从已经集成好的 Knife4j 进行入手。

自定义扩展实现OperationCustomizer 接口进行注入自定义的描述信息,这样正常写业务代码,无需额外注解实现解耦,即便后续要升级 swagger 版本也不会导致全篇幅修改,其实这块内容没啥难点,Knife4j 官网提供了大量的文档参考一下就能搞出来,主要就是解决问题的思路,如何选择一种便捷高效的解决工作的种种问题,从而让团队协作更融洽。

下面是自定义实现,目标就是扫描 controller 指定注解然后进行追加到描述中(纯 AI 生成勿喷哈哈哈哈哈)。

import io.swagger.v3.oas.models.Operation;
import org.springdoc.core.customizers.GlobalOperationCustomizer;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.method.HandlerMethod;

/**
 * 自动扫描 Controller 上的 @PreAuthorize 注解并添加到 Swagger 描述中
 */
public class PreAuthorizeOperationCustomizer implements GlobalOperationCustomizer {

    @Override
    public Operation customize(Operation operation, HandlerMethod handlerMethod) {
        // 1. 获取方法上的注解,如果没有则获取类上的
        PreAuthorize preAuthorize = handlerMethod.getMethodAnnotation(PreAuthorize.class);
        if (preAuthorize == null) {
            preAuthorize = handlerMethod.getBeanType().getAnnotation(PreAuthorize.class);
        }

        // 2. 如果存在注解,则解析并追加到文档
        if (preAuthorize != null) {
            String criteria = preAuthorize.value();
            String permission = extractPermission(criteria);

            // 修改描述 (Description)
            String desc = operation.getDescription() == null ? "" : operation.getDescription();
            String securityInfo = String.format("<div style='margin-top:10px;color:#e6a23c;'><strong>所需接口权限:</strong><code>%s</code></div>", permission);
            operation.setDescription(desc + securityInfo);

            // 可选:同时修改标题 (Summary),让权限在左侧菜单或列表中一眼可见
//            String summary = operation.getSummary() == null ? "" : operation.getSummary();
//            operation.setSummary(summary + " (权限: " + permission + ")");
        }

        return operation;
    }

    /**
     * 解析 SpEL 表达式,提取权限标识
     * 例如将 @ss.hasPermission('user:info:query') 转换为 user:info:query
     */
    private String extractPermission(String value) {
        if (value == null || value.isEmpty()) {
            return "";
        }
        // 匹配单引号中间的内容
        if (value.contains("'")) {
            return value.substring(value.indexOf("'") + 1, value.lastIndexOf("'"));
        }
        return value;
    }
}

然后注入到容器中即可生效。

    /**
     * 文档显示所需接口权限标识符信息
     *
     * @return 自定义处理器
     */
    @Bean
    public GlobalOperationCustomizer preAuthorizeSummaryCustomizer() {
        return new PreAuthorizeOperationCustomizer();
    }

最终效果

实现 Knife4j swagger 文档自动追加注解描述

总结

举一反三,在很多场景下为了解决通用性问题,最好从框架层面解决,这样能够增加可维护性,而不是每个团队成员都要去做一些纯体力活。

文章目录