1. 为什么这套组合至今仍值得聊
但凡做过Java后端的人,对Spring Security大概都有一种又爱又恨的复杂情绪。爱的是它确实能把认证授权这套东西给你兜住,恨的是它的配置方式在不同版本之间反复横跳,网上搜到的教程十篇里有八篇跑不起来。而Swagger这个接口文档工具,本身跟Spring Security是两条平行线,可一旦你把它们塞进同一个Spring Boot工程里,问题就来了——Swagger的那一堆静态资源和接口路径全被Security拦截了,打开文档页面直接401,或者页面能打开但接口列表加载不出来。
这个组合的坑,我在实际项目里踩过不止一次。标题里提到的Swagger 2.9.2这个版本号很关键,因为2.9.x是Swagger 2系列的最后一个大版本,之后Springfox就基本停更了,很多人转向了springdoc-openapi。但现实情况是,大量存量项目、公司内部框架、老系统升级,仍然跑在Swagger 2.9.2上。所以把这套组合的整合逻辑彻底搞清楚,不是怀旧,是实打实的生产需要。
这篇文章适合谁看?如果你正在维护一个Spring Boot 2.x的项目,用着Spring Security做权限控制,同时想接入Swagger做接口文档,结果被各种拦截规则搞得头大,那这篇就是写给你的。我会把整个整合思路、配置细节、踩坑记录全部摊开讲,代码可以直接抄。如果你用的是Spring Boot 3.x或者已经迁移到springdoc,部分配置方式会不一样,但底层的Security过滤链思路是相通的,同样有参考价值。
先说清楚一个前提:Spring Security的版本和Spring Boot的版本是绑定的。Spring Boot 2.1.x到2.3.x这个区间,对应的Spring Security是5.1到5.3左右,这个区间的配置方式以WebSecurityConfigurerAdapter为主。标题里标注的时间是2020年6月,那会儿主流就是Spring Boot 2.2.x或2.3.x配Spring Security 5.x,Swagger用2.9.2。我下面所有的配置和代码,都基于这个版本组合来展开,不会混入Spring Boot 3的写法,避免读者混淆。
2. 整合前必须想清楚的几个设计问题
2.1 为什么Swagger会被Security拦截
很多人第一次遇到Swagger页面打不开,第一反应是Swagger配置错了。其实不是。Spring Security默认会拦截所有进入应用的HTTP请求,它不管你这些请求是给业务接口的还是给文档页面的。Swagger在运行时会在你的应用里注册一批资源和端点,主要包括这几类:
swagger-ui.html这个页面本身/swagger-resources/**下的配置资源/v2/api-docs这个核心的文档数据接口/webjars/**下的前端静态资源
这些路径如果不显式放行,Security就会按照默认规则要求认证,结果就是浏览器请求这些资源时被重定向到登录页或者直接返回401。所以整合的核心动作,就是在Security的过滤链里给这些路径开白名单。
2.2 放行策略的两种思路对比
放行Swagger相关路径,常见有两种做法,我把它俩摆在一起对比一下,你就知道该选哪个了。
| 策略 | 做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 全路径放行 | 把Swagger所有相关路径加入permitAll | 配置简单,一次到位 | 生产环境文档也暴露 | 开发测试环境 |
| 按环境开关 | 通过Profile控制Swagger是否启用,Security配置联动 | 生产安全 | 配置稍复杂 | 正式项目 |
我的建议是:开发阶段用第一种快速跑通,项目上线前一定要切到第二种。因为Swagger文档暴露的接口信息,对攻击者来说就是一张地图,生产环境绝对不应该对外开放。后面我会给出按环境开关的完整实现。
2.3 版本兼容性这个隐形炸弹
Springfox Swagger 2.9.2对Spring Boot的版本是有要求的。如果你用的是Spring Boot 2.6及以上,会发现启动直接报错,原因是Spring Boot 2.6把路径匹配策略默认从AntPathMatcher改成了PathPatternParser,而Springfox 2.9.2不兼容这个变化。解决办法是在配置文件里把匹配策略改回去:
spring: mvc: pathmatch: matching-strategy: ant_path_matcher这个坑我在一个升级项目里遇到过,当时排查了大半天,最后发现就是这一行配置的事。所以你在动手之前,先确认自己的Spring Boot版本,如果是2.6+,记得加上这行。
3. 依赖引入与基础配置落地
3.1 pom依赖的精确写法
依赖这块没什么花活,但版本号要对齐。Spring Boot的parent统一管理版本,Swagger的两个依赖版本保持一致。
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.3.12.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency> </dependencies>这里有个细节值得说:springfox-swagger2是核心的文档生成库,springfox-swagger-ui是那个可视化页面的前端包。两个都要引,少一个页面就打不开。另外,2.9.2这个版本依赖了比较老的guava和spring-plugin-core,在某些项目里可能跟其他依赖冲突,如果启动时报NoSuchMethodError之类的错误,八成是依赖冲突,用mvn dependency:tree排查一下。
3.2 Swagger配置类的写法
Swagger的配置类本身不复杂,但有几个点容易写错。我直接给一份能跑的:
@Configuration @EnableSwagger2 @EnableConfigurationProperties(SwaggerProperties.class) public class SwaggerConfig { @Bean public Docket apiDocket(SwaggerProperties properties) { return new Docket(DocumentationType.SWAGGER_2) .enable(properties.isEnabled()) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.example.demo.controller")) .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("业务接口文档") .description("接口说明与调试入口") .version("1.0.0") .build(); } }@EnableSwagger2这个注解必须加,它是开启Swagger功能的开关。basePackage要指向你实际的Controller所在包,写错了文档里就是空的。.enable()这个方法接收一个布尔值,用来控制Swagger是否生效,这个就是后面做环境开关的关键。
3.3 用配置属性控制Swagger开关
为了让Swagger能在不同环境灵活开关,我习惯把它抽成一个配置属性类:
@ConfigurationProperties(prefix = "swagger") public class SwaggerProperties { private boolean enabled = true; private String title = "接口文档"; private String version = "1.0.0"; // getter 和 setter 省略 }然后在application-dev.yml里写swagger.enabled: true,在application-prod.yml里写swagger.enabled: false。这样打包上线的时候,生产环境的Swagger自动关闭,连文档接口都不会注册,Security那边也就不需要为它操心了。这个设计比在Security里硬编码放行路径要优雅得多,因为它从源头上就把Swagger关掉了。
4. Security配置的核心细节拆解
4.1 放行路径的完整清单
这是整个整合里最关键的一步。Swagger 2.9.2需要放行的路径,我整理成一张表,你照着配就行:
| 路径 | 作用 | 是否必须放行 |
|---|---|---|
/swagger-ui.html | 文档主页面 | 是 |
/swagger-ui/** | 新版UI资源路径 | 是 |
/swagger-resources/** | 分组配置资源 | 是 |
/v2/api-docs | 文档数据接口 | 是 |
/webjars/** | 前端静态资源 | 是 |
/doc.html | 部分UI入口 | 视情况 |
这里要特别提醒:/v2/api-docs这个路径,如果你在Swagger配置里自定义了groupName或者用了多Docket分组,实际路径会变成/v2/api-docs?group=xxx这种形式,但放行的时候只需要放行/v2/api-docs这个前缀即可,查询参数不影响路径匹配。
4.2 WebSecurityConfigurerAdapter的配置写法
基于Spring Security 5.x的经典写法,我给出完整配置:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers( "/swagger-ui.html", "/swagger-ui/**", "/swagger-resources/**", "/v2/api-docs", "/webjars/**" ).permitAll() .antMatchers("/api/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin().permitAll() .and() .httpBasic(); } }逐段解释一下。csrf().disable()这行在前后端分离或者纯接口项目里基本是标配,因为CSRF防护主要针对基于Session的表单场景,接口项目开着反而会导致POST请求被拦。authorizeRequests()开启授权配置,后面的antMatchers按顺序匹配,越具体的路径越要写在前面。anyRequest().authenticated()是兜底规则,除了上面放行的,其他一律要认证。
4.3 为什么放行了还是打不开
这是最高频的问题。放行路径配了,但页面还是打不开,通常有这几个原因:
第一个,路径写错了。比如把/swagger-resources/**写成了/swagger-resource/**,少个s,匹配不上。这种拼写错误肉眼很难发现,建议直接从浏览器Network面板里看实际请求的路径,复制过来。
第二个,放行规则的位置不对。antMatchers是按声明顺序匹配的,如果你把anyRequest().authenticated()写在了放行规则前面,那后面的放行就永远不生效。这个顺序问题坑过很多人。
第三个,Spring Boot 2.6+的路径匹配策略问题,前面提过了,加那行ant_path_matcher配置。
第四个,如果你用了自定义的Filter或者拦截器,可能在Security之前就把请求拦了。排查的时候可以在Filter里打日志,看请求到底走到哪一步被拦下的。
5. 完整实操流程与验证
5.1 从零搭建的步骤清单
我把整个搭建过程拆成可执行的步骤,你按顺序来:
- 创建Spring Boot项目,引入web、security、swagger三个依赖
- 写SwaggerConfig配置类,加上
@EnableSwagger2 - 写SwaggerProperties配置属性类,支持环境开关
- 写SecurityConfig,放行Swagger相关路径
- 写一个测试Controller,加几个接口
- 启动项目,访问
http://localhost:8080/swagger-ui.html - 验证页面能打开、接口列表能加载、接口能调试
5.2 测试Controller的写法
为了验证整合效果,写个简单的Controller:
@RestController @RequestMapping("/api/user") @Api(tags = "用户管理") public class UserController { @ApiOperation("查询用户列表") @GetMapping("/list") public List<String> list() { return Arrays.asList("user1", "user2"); } @ApiOperation("根据ID查询用户") @GetMapping("/{id}") public String getById(@PathVariable Long id) { return "user-" + id; } }@Api和@ApiOperation这两个注解是Swagger用来生成文档描述的,加上之后页面里就能看到中文说明。注意/api/user这个路径,如果它不在放行清单里,那访问接口本身是需要认证的,但文档页面能正常展示接口定义,这是正常的——文档展示和接口调用是两回事。
5.3 验证过程中的关键观察点
启动之后,打开浏览器访问文档页面,重点看这几个地方:
- 页面标题是否显示你配置的title
- 接口分组是否按Controller正确归类
- 每个接口的说明文字是否显示
- 点击"Try it out"能否正常发起请求
如果页面能打开但接口列表是空的,八成是basePackage配错了,或者Controller没加@RestController。如果接口列表有但点击调试报401,说明接口路径本身需要认证,这是Security在正常工作,不是bug。
6. 常见问题排查速查表
我把实际项目中遇到的高频问题整理成表,方便你对照排查:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 页面404 | 依赖没引全或路径写错 | 检查swagger-ui依赖和访问路径 |
| 页面401 | 放行路径没配或顺序错 | 检查antMatchers配置 |
| 页面能开但列表空 | basePackage配错 | 核对Controller包路径 |
| 启动报错NoSuchMethod | 依赖冲突 | 用dependency:tree排查 |
| 2.6+启动失败 | 路径匹配策略变了 | 加ant_path_matcher配置 |
| 接口调试跨域失败 | 没配CORS | 在Security里开启cors |
关于跨域这个补充一句,Swagger页面调试接口时,如果前后端不同源,需要在Security配置里加上.cors(),并且提供一个CorsConfigurationSource的Bean。这个在前后端分离项目里几乎必配。
7. 几个我踩过的坑和实操心得
第一个坑,关于@EnableWebSecurity和@EnableGlobalMethodSecurity的区别。前者是开启Web安全,后者是开启方法级安全注解(比如@PreAuthorize)。如果你只做URL级别的权限控制,加@EnableWebSecurity就够了。我见过有人在方法级注解不生效时反复检查注解,最后发现是忘了加@EnableGlobalMethodSecurity(prePostEnabled = true)。
第二个坑,Swagger的@ApiModelProperty注解在实体类上写的时候,example属性给的值如果类型不对,页面调试时会报参数转换错误。比如字段是Long类型,你example写了个字符串,点调试就炸。这个细节很小但很烦人,建议example的值跟字段类型严格对应。
第三个坑,关于Security的密码编码器。Spring Security 5之后,如果你配置了内存用户或者数据库用户,密码必须用BCryptPasswordEncoder加密存储,明文密码会报"Encoded password does not look like BCrypt"的警告,虽然不一定报错,但认证会失败。这个在整合Swagger时容易被忽略,因为你的注意力都在放行路径上。
第四个心得,关于文档分组。项目大了之后,一个文档页面塞几百个接口,找起来很痛苦。用多个Docket做分组,按业务模块拆开,体验会好很多。分组的写法是在Docket上设置groupName,然后每个分组指定不同的basePackage或路径匹配规则。
第五个心得,Swagger的注解虽然方便,但不要为了文档而文档。我见过有人在每个字段上都堆一堆@ApiModelProperty,写得比业务代码还长。我的原则是:接口级别的说明必须写清楚,字段级别的说明按需写,核心字段和容易误解的字段写,其他可以省略。文档是给人看的,不是给注解数量凑数的。
8. 生产环境的安全收尾
开发阶段跑通只是第一步,上线前必须做几件事。第一,把swagger.enabled在生产配置里设为false,从源头关闭。第二,如果因为某些原因生产也要保留文档(比如内部系统),那一定要给文档路径加上独立的认证,不能简单permitAll。第三,检查Security的兜底规则,确保没有意外的路径被放行。第四,把Swagger相关的依赖在打包时排除掉,减小包体积,也避免潜在的安全扫描告警。
我个人的习惯是,在application-prod.yml里明确写swagger.enabled: false,同时在Security配置里用@Profile("!prod")把放行规则也隔离掉,双保险。这样即使有人误改了配置属性,Security层面也不会放行文档路径。
这套组合的整合,说到底就是两件事:让Swagger的资源路径通过Security的检查,以及让Swagger在正确的环境里开关。把这两件事想明白,剩下的都是配置细节。我上面给的代码和配置,在Spring Boot 2.3.x + Spring Security 5.3 + Swagger 2.9.2这个组合下是验证过的,你直接拿去改改包名就能用。如果你的版本组合不一样,重点检查路径匹配策略和Security的配置方式,这两个是版本差异最大的地方。