做后端接口开发,Swagger基本是每个项目都离不开的东西。但有个场景我敢说绝大多数人都踩过或者迟早会踩:接口加了Token鉴权之后,Swagger UI里那些带锁的接口一点就返回401,或者页面上压根找不到Authorize按钮,好不容易找到了、填入Token之后发送请求,请求头里还是什么都没有。
这个需求看着简单——“Swagger中添加Token验证”,实际上牵扯到OpenAPI的安全模型、Swagger不同依赖版本之间的差异、后端拦截器的放行规则等多个环节。如果只照着网上一段代码抄,很容易出现“明明别人能用,我这边就是不行”的尴尬。这篇文章我会把整套配置从原理到实操完整跑一遍,把每个参数为什么这么写、有哪些坑、什么排查顺序都讲清楚,适合正在用Spring Boot做接口开发、想快速把Swagger调试环境打通的开发者参考。
1. 这个需求到底在解决什么
1.1 先从右上角那个Authorize按钮说起
Swagger UI右上角的Authorize按钮,本质上是OpenAPI规范里定义的Security Scheme在界面层的呈现。OpenAPI把接口安全分成了几大类:HTTP认证(Bearer、Basic)、API Key、OAuth2、OpenID Connect。Swagger UI拿到后端生成的OpenAPI文档后,根据文档里声明的安全方案,自动渲染出对应的输入界面。值得注意的是,并不是每个项目点开Authorize都会有一个输入Token的框,框长什么样完全取决于你在后端配置的SecurityScheme类型。
填完Token之后,Swagger UI并不是自己保存一个“魔法值”,而是把这个值映射为请求头,在后续每个接口请求发出时自动携带。Bearer类型默认会变成Authorization: Bearer <token>,API Key类型则按你指定的Header名或Query参数名传递。这个机制一定要先理解,后面所有配置都不会迷糊。
理解了这一点你就会发现,这个需求绝不是在Swagger页面上加一个输入框那么简单。你需要做两件事:第一,告诉Swagger UI这个项目支持哪种安全方案;第二,保证填充后的Token确实能被后端请求链路识别。第一件事通过配置实现,第二件事才是大多数人卡壳的地方。
1.2 为什么很多人配置完依然401
我接手过一个遗留项目,Swagger页面能打开,Authorize按钮也有,填入了正确的Token,结果每个接口仍然返回401。排查到最后发现,问题根本不在Swagger配置本身,而是网关根本没有把Swagger请求链路放行。Swagger UI在加载OpenAPI文档时发的请求被拦截器拦了下来,页面虽然渲染出来了,但实际上拿到的是残缺不完整的文档,UI里的很多状态都不对。
还有一种常见情况:Token确实带上去了,但后端的鉴权过滤器除了Authorization之外,还强制要求额外的请求头(比如设备类型、客户端版本之类的标记),Swagger UI初始化时只带了Authorization,于是接口照样被拒。
所以“Swagger中加Token验证”从表面看是个UI配置问题,实际操作起来需要OpenAPI文档、UI交互、后端拦截链三项对齐。任何一环脱节,最终表现都是“按钮没反应”或者“请求仍401”。
2. 版本选型要分清:Springfox和springdoc是两套思路
2.1 老项目还在用Springfox该怎么配
很多老项目用的是Springfox,尤其Spring Boot 2.x时代。它的配置思路是生成一个Docket Bean,在Bean上同时声明SecurityScheme和SecurityContext。注意,Springfox的SecurityScheme默认只负责在UI上显示录入框和存储值,想要让UI在请求时自动携带Token,还必须额外配置SecurityContext,在里面显式指定哪些操作需要应用这个安全方案。
我见过不少项目配置里漏掉了securityContext(),结果界面上有锁形按钮,填入Token后请求头始终带不上去。Springfox的配置如下:
@Bean public Docket createRestApi() { return new Docket(DocumentationType.OAS_30) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.example.controller")) .paths(PathSelectors.any()) .build() .securitySchemes(Collections.singletonList(apiKey())) .securityContexts(Collections.singletonList(securityContext())); } private ApiKey apiKey() { return new ApiKey("BearerAuth", "Authorization", "header"); } private SecurityContext securityContext() { return SecurityContext.builder() .securityReferences(Collections.singletonList(new SecurityReference( "BearerAuth", new AuthorizationScope[0] ))) .operationSelector(o -> true) .build(); }这里重点解释一下ApiKey的三个参数:分别是安全方案的名称、Header名、位置。名称BearerAuth必须与SecurityReference里的名称完全一致,否则Swagger UI识别不到,Authorize按钮填完不会对任何接口生效。从Springfox 3.0开始,DocumentationType.OAS_30对应OpenAPI 3.0规范,和springdoc最终产出的OpenAPI JSON是同一种东西,但两者的构建方式完全不同,迁移的时候不能只换依赖。
2.2 新项目直接用springdoc更省心
springdoc-openapi是现在的主流选择,尤其在Spring Boot 3.x下,官方只支持springdoc-openapi-starter-webmvc-ui,Springfox已经停止维护,在Spring Boot 3.x环境下依赖都冲突。springdoc的一大优势是配置起来比Springfox直观得多:只要在配置类里声明一个OpenAPI Bean,通过addSecurityItem和addSecuritySchemes两个方法就能同时完成UI显示和请求携带的设置。
2.2.1 直接能用的最小配置
@Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { String schemeName = "bearerAuth"; return new OpenAPI() .info(new Info() .title("演示项目接口文档") .description("用于演示Swagger中Token验证配置的接口文档") .version("v1.0.0")) .addSecurityItem(new SecurityRequirement().addList(schemeName)) .components(new Components() .addSecuritySchemes(schemeName, new SecurityScheme() .name("Authorization") .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT") )); } }这段代码配置完后,刷新Swagger UI页面,右上角就会出现Authorize按钮,点击后是一个Token输入框。此后任意接口请求发出时,Swagger UI都会自动带上Authorization: Bearer <token>。
2.2.2 各项参数为什么这么写
new SecurityRequirement().addList(schemeName)是全局安全要求,等价于告诉Swagger UI“当前文档里所有接口都应用这套鉴权”。这个如果不写,界面虽然可能显示输入框,但接口请求不会自动附带Token,因为UI不知道哪些接口需要它。
type(SecurityScheme.Type.HTTP)、scheme("bearer")、bearerFormat("JWT")组合起来,会渲染出一个Bearer Token输入框,并提示格式。这里有个很常见的误区:如果你用的是name("Authorization") + type(APIKEY) + in(HEADER)的写法,确实也能实现往Header里塞Token,但Swagger UI不会自动加Bearer前缀,你得手动把Token填成“Bearer xxx”才行。用HTTP+Bearer组合则没有这个问题,语义也更清晰。
2.2.3 Springfox和springdoc的核心差异
| 维度 | Springfox | springdoc |
|---|---|---|
| 维护状态 | 已停止维护 | 持续维护 |
| Spring Boot 3.x兼容 | 不支持 | 支持 |
| 主要配置对象 | Docket | OpenAPI |
| 安全配置方式 | securitySchemes + securityContexts | addSecurityItem + addSecuritySchemes |
| 默认文档路径 | /v2/api-docs 或 /v3/api-docs | /v3/api-docs |
2.3 依赖坐标上的坑
很多人直接从网上一段旧教程复制依赖,结果项目启动直接报错,最常见的是NoClassDefFoundError。这是因为Springfox引用了Spring Boot 2.x内部的一些类,在Spring Boot 3.x下已经被移除了。Spring Boot 2.7及以前的项目选型比较自由,Spring Boot 3.x只能走springdoc。
springdoc的依赖坐标在不同时期也调整过,Spring Boot 3.x必须用:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.x.x</version> </dependency>版本号以Maven中央仓库实际发行版本为准,不要盲目追求最新,也别用太老的版本。我踩过的坑是:在Spring Boot 3.2.x下用了很早的springdoc 2.0.0,Swagger UI页面可以打开但OpenAPI JSON加载报404,升级到2.x的中期版本后才正常。
3. 核心配置实操:把Authorize按钮和Token携带一次搞定
3.1 带分组和配置项读取的完整方案
如果项目里接口比较多,一般还会给Swagger分组,这时候Token验证的配置就需要稍微调整一下。下面的例子把分组定义和Token安全方案放在同一个配置类里,保证所有分组默认都会显示Authorize按钮:
@Configuration public class OpenApiConfig { private final String schemeName = "bearerAuth"; @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("演示项目接口文档") .description("用于演示Swagger中Token验证配置") .version("v1.0.0")) .addSecurityItem(new SecurityRequirement().addList(schemeName)) .components(new Components() .addSecuritySchemes(schemeName, new SecurityScheme() .name("Authorization") .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))); } @Bean public GroupedOpenApi publicApi() { return GroupedOpenApi.builder() .group("public") .pathsToMatch("/api/public/**") .build(); } @Bean public GroupedOpenApi adminApi() { return GroupedOpenApi.builder() .group("admin") .pathsToMatch("/api/admin/**") .addOpenApiCustomizer(openApi -> openApi.addSecurityItem(new SecurityRequirement().addList(schemeName))) .build(); } }这里要注意一个细节:GroupedOpenApi默认继承全局OpenAPI里的SecurityRequirement,所以正常情况下只需要在customOpenAPI里配一次。只有在某些特殊场景下(比如某个分组不想走统一鉴权),才需要在分组级通过addOpenApiCustomizer调整。
3.2 参数映射:配置代码如何变成UI行为
很多人配置完了不知道自己的代码在UI上生成了什么,这里直接把对应关系说清楚:
| 后端配置 | Swagger UI表现 |
|---|---|
| type(HTTP) + scheme("bearer") | 弹出Authorize窗口,显示一个Token输入框,不要求输Bearer前缀 |
| type(HTTP) + scheme("basic") | 弹出账号密码输入框 |
| type(APIKEY) + name("Authorization") + in(HEADER) | 弹出Key输入框,填什么就原样放进请求头 |
| addSecurityItem(全局) | 所有接口右侧显示锁形图标 |
| 不配addSecurityItem,只用方法级注解 | 只有注解标记的接口显示锁形图标 |
正因为APIKEY方式在UI上“填什么就是什么”,很多不懂原理的人在这里翻车:填了Bearer eyJxxx,请求头里变成了两个Bearer前缀,或者填了Token却完全没带前缀导致后端解析失败。用HTTP+Bearer方式就没有这个问题,UI上明确提示你只需要粘贴Token本体。
3.3 只给部分接口加锁:方法级SecurityRequirement
不是所有接口都需要Token,比如登录接口、健康检查接口。很多项目希望Swagger里保持开放状态,只有业务接口加锁。这时候有两种做法。
第一种:全局OpenAPI里不写addSecurityItem,只保留components里的SecurityScheme定义。然后在需要的Controller类上或方法上添加注解:
@RestController @RequestMapping("/api/user") public class UserController { @Operation(security = @SecurityRequirement(name = "bearerAuth")) @GetMapping("/info") public UserInfo info() { return userService.getInfo(); } @PostMapping("/login") public LoginResult login(@RequestBody LoginRequest request) { return authService.login(request); } }这样登录接口没有锁图标,/info接口有锁图标,Swagger UI在点击/info时要求携带Token。
第二种:全局配置里仍然写了addSecurityItem,这时候你再在方法上写@SecurityRequirement(name = "bearerAuth")不会覆盖全局,结果锁还是全接口生效。想让某个接口从鉴权里剔除,用空数组形式声明也许可以做到,但不同版本对这个注解的语义解析略有差异,有遇到直接在全局开放、局部加锁就好,尽量避免逆向操作。
我个人在实际项目里推荐第一种:全局不锁,局部加锁。好处是登录接口、文档本身都可以保持可用,等接口调通了再统一收紧。
4. 光有按钮不够:后端拦截和请求头细节
4.1 Swagger静态资源路径的放行规则
Swagger UI能打开页面,并不代表它的所有内部请求都通畅。Spring Boot项目里绝大多数会注册拦截器做Token校验,如果你的拦截器没有把Swagger相关路径放行,会出现页面能打开但OpenAPI文档加载失败的情况。UI一旦拿不到文档,Authorize按钮就渲染不出来。
常见的放行路径有这些:
/swagger-ui.html /swagger-ui/** /v3/api-docs/** /v3/api-docs/swagger-config /webjars/**如果你在WebMvcConfigurer里注册了拦截器,推荐这样写:
registry.addInterceptor(jwtInterceptor()) .addPathPatterns("/**") .excludePathPatterns( "/swagger-ui.html", "/swagger-ui/**", "/v3/api-docs/**", "/v3/api-docs/swagger-config" );注意:springdoc允许通过配置修改api-docs路径,如果项目里自定义了springdoc.api-docs.path,放行规则也要跟着改。不要写死。
4.2 网关和CORS里的OPTIONS预检请求
如果项目接了网关统一鉴权,Swagger的跨域预检请求容易出问题。Swagger UI在加载文档或发送接口请求前,浏览器可能先发一个OPTIONS请求探测CORS支持。如果你的鉴权过滤器把OPTIONS也拦了,或者网关不返回允许的CORS响应头,Swagger页面可能看起来正常,但实际请求全被卡在预检这一步。
经验做法是:在开发环境配置里对OPTIONS请求统一放行,或者让网关把Swagger调试的相关路径排除出严格鉴权。等联调阶段再恢复完整鉴权,并不影响安全。
4.3 Token过期后Swagger UI的迷惑行为
Swagger UI的Authorize录入的Token会存在浏览器LocalStorage里,它不会自动感知Token过期。所以很多人都遇到过这种情况:后端Token半小时过期,Swagger里还是填的半小时前的旧值,每个接口都401。这不是配置坏了,就是Token过期了。
我自己的做法分为两步:一是点开Authorize按钮,清掉旧值,重新粘贴新Token;二是测试环境下把JWT的过期时间调长,比如设置成一个小时甚至更长,省得在Swagger里反复粘贴。需要提醒的是,生产环境的真实Token不要往Swagger UI里粘贴,本地调试建议用专用测试账号的Token。
4.4 用浏览器开发者工具验证Token是否真的带上了
排查Swagger请求问题,最快的办法不是看配置,而是打开浏览器F12开发者工具,切到Network面板,刷新页面后发送一个接口请求,查看请求头里的Authorization字段是否存在。
这个验证能快速把问题分成两种:如果请求头里没有Authorization,问题出在Swagger UI的安全配置上;如果有Authorization但后端还是401,问题出在后端鉴权解析或过滤器链路上。大多数人说“Swagger加Token验证没效果”,其实八个请求里只有两三个带了Token,剩下的是缓存的原因。
5. 填了Token还是401?常见问题排查实录
5.1 问题速查表
| 症状 | 可能原因 | 解决方案 |
|---|---|---|
| Authorize按钮不显示 | OpenAPI配置里没声明SecurityScheme,或Swagger文档加载失败 | 检查配置类是否生效、Swagger路径是否被拦截 |
| 填了Token但请求头没带 | 全局SecurityRequirement缺失,或scheme名称不一致 | 补addSecurityItem,确认名称统一 |
| 填了Token仍然401 | Token过期,或后端过滤器额外要求Header | 更新Token,查看后端拦截器逻辑 |
| 页面能开但文档一直转圈 | /v3/api-docs/路径被网关或拦截器拦截 | 放行api-docs相关路径 |
| 部分接口没有锁图标 | 分组配置漏了,或方法级注解没生效 | 检查分组配置,确认注解name与scheme一致 |
5.2 两个典型排查案例
第一个案例来自一个使用Springfox的模拟项目。表现是Authorize按钮有,填入Token后接口请求头完全没变化。排查后发现配置类里只调用了securitySchemes(apiKey()),漏掉了securityContexts(securityContext())。Springfox和springdoc不同,它必须依靠SecurityContext建立“锁”和“操作”的关联关系。补上securityContext()后,请求头立刻正常携带。
第二个案例场景是接口文档正常,Authorize按钮也有,填了Token后Header里也能看到Authorization,但后端返回401。最后定位到网关层:网关把Swagger UI和api-docs路径都放行了,但业务接口路径向下游服务传递时,把Authorization头给剥离了。这个问题不在Swagger配置里,而是链路中转环节的Header透传问题。
5.3 排查问题时的正确顺序
遇到“Swagger里Token验证不生效”,按这个顺序查,效率最高:
- 打开浏览器Network面板,确认请求头里有没有Authorization。
- 确认请求头里有Token,再看后端返回是401还是403还是500,区分鉴权失败与权限不足。
- 确认Token本身没有过期,最好拿同一个Token到命令行工具里直接调一次接口,排除Swagger干扰。
- 确认拦截器或者网关是否放行了Swagger相关路径以及OPTIONS预检。
- 最后才检查OpenAPI配置里SecurityScheme和SecurityRequirement的关联关系。
很多时候问题出在第4步或者第2步,而不是Swagger配置本身。别一上来就怀疑配置,先用Network面板把事实查清楚。
6. 进阶玩法:分组配置、配置驱动和调试技巧
6.1 多分组API单独控制Token验证
项目接口多了以后,通常会把Swagger按业务模块分组。这时候Token验证的需求也会跟着变:对外模块要求严格鉴权,内部模块可能只要一个固定的调试Token。分组配置里,每个GroupedOpenApi可以单独调整安全策略,比如给某些分组去掉全局安全限制:
@Bean public GroupedOpenApi internalApi() { return GroupedOpenApi.builder() .group("internal") .pathsToMatch("/api/internal/**") .addOpenApiCustomizer(openApi -> openApi.security(new ArrayList<>())) .build(); }需要说明的是,Swagger UI的Authorize按钮是全局的,切换分组并不会让Token分开存储。如果你在多个分组下用不同的Token测试,只能手动重填。这个限制是Swagger UI本身的交互逻辑决定的,后端配置能做的边际有限。
6.2 用application.yml驱动Swagger配置
把Swagger的title、description、version这些信息放到配置文件里,通过@ConfigurationProperties注入,好处是不同环境可以只改配置不打新包。安全方案的名称和类型也可以一并配置化:
@Data @Component @ConfigurationProperties(prefix = "swagger") public class SwaggerProperties { private String title; private String description; private String version; private String securitySchemeName; }配置类改成读取这个Properties实例,这样开发、测试、预发环境各自用不同的描述信息或安全方案名称,都能在不改代码的情况下完成。不过要注意:如果多个环境的安全方案类型本身不同(比如开发用Bearer,测试用APIKey),配置属性也要跟着调整,不只是名称。
6.3 开发调试时更好用的小技巧
调试Swagger接口时,我最常用的一个技巧是:本地测试环境把JWT过期时间设长一些,避免频繁重新粘贴Token。另一个技巧是利用浏览器的Console直接读取Swagger UI的本地存储,快速确认当前存储的Token和界面显示的是否一致。偶尔也会遇到Swagger UI界面状态异常的情况,清除一下该站点的LocalStorage再刷新就好。
如果项目支持OAuth2授权码模式,还可以在SecurityScheme里配置OAuth2授权URL,让Swagger UI自带登录换Token的流程。这样开发人员不再需要手动拿Token,直接在Authorize弹窗里点授权即可。不过这个需要后端真的支持对应的授权流程,不是纯UI配置能解决的,普通Bearer Token项目不需要折腾这个。
6.4 会话型认证与Header型Token的边界提醒
有一种项目用的是Session Cookie认证而不是Token。这种情况下Swagger UI的Authorize按钮并不天然支持往Cookie里塞值,硬配APIKey类型往Header里塞Cookie也是别扭的做法。这种项目想用Swagger调试,更合理的思路是提供一个开发环境专用的登录接口,先调登录接口建立会话,再调试业务接口。如果你用了Token方案但项目里还有部分旧接口依赖Session,Swagger UI能覆盖的也只是Token这一部分,会话部分还是要依赖登录接口先打通。
在跨域环境下,如果用了Cookie会话,还会受SameSite策略影响,跨端口调试往往莫名其妙地调不通。把测试环境改成同域部署,或者改用Authorization Header传Token,都可以规避这一类问题。
最后在实际操作层面我还有一点体会:配置Swagger里的Token验证,不是写完配置类就结束了。真正稳定好用的调试环境,需要把拦截器放行、Token过期处理、分组差异这三件事一起考虑进去。把这几件事理顺了,团队里的每个开发都能省下大量和鉴权纠缠的时间,这比只给Swagger加一个Authorize按钮有价值得多。