1. 从一个真实案例说起:Swagger 页面怎么就变成“后门”了
去年帮一个朋友做安全巡检,他用 Spring Boot 写了个内部管理系统,部署在公网一台云主机上,自认为“接口都加了 JWT 鉴权,稳得很”。我随手访问了一下/swagger-ui/index.html,页面直接弹出来了,所有 Controller 的接口路径、请求参数、返回结构、甚至部分示例数据一览无余。更麻烦的是,有几个接口的鉴权拦截器配置漏了路径匹配,等于说攻击者拿着 Swagger 页面当“菜单”,挨个试就能找到未授权的写接口。
这不是个例。Spring Boot 生态里,Swagger(现在主流是 springdoc-openapi 或 springfox)几乎是标配的接口文档工具,开发阶段确实好用,但很多人打包上线时忘了关,或者关了 UI 却没关/v3/api-docs这类元数据端点。结果就是:接口文档变成了攻击者的侦察地图。
这篇内容就围绕Spring Boot Swagger 未授权访问漏洞展开,把原理、真实风险、检测方法、修复方案、以及修复过程中容易踩的坑,一次性讲透。适合正在用 Spring Boot 做后端开发的同学、做安全测试的同学,以及负责上线前安全把关的运维和测试同学。不管你是刚接触 Spring Boot 的新手,还是已经写过几个企业级项目的老手,这里面的排查思路和配置细节都能直接拿去用。
先说清楚一个概念:Swagger 未授权访问本身不是一个“漏洞编号”意义上的 CVE,它属于配置缺陷导致的信息泄露与攻击面扩大。单独看,它可能只是泄露接口结构;但结合其他弱点(比如某个接口鉴权缺失、参数校验不严),它就成了完整的攻击链入口。所以别觉得“不就是个文档页面嘛”,真正出问题的时候,它就是第一块倒下的多米诺骨牌。
2. Swagger 在 Spring Boot 里到底是怎么跑起来的
2.1 两代主流方案:springfox 与 springdoc-openapi
要理解漏洞,先得知道 Swagger 在 Spring Boot 项目里是怎么被“挂”上去的。目前市面上主要有两套:
- springfox:老牌方案,对应 Swagger 2.x 规范,依赖
springfox-swagger2和springfox-swagger-ui。很多老项目、若依(RuoYi)早期版本用的就是它。它的典型特征是访问/swagger-ui.html或/swagger-ui/index.html,元数据端点是/v2/api-docs。 - springdoc-openapi:新一代方案,对应 OpenAPI 3.x 规范,依赖
springdoc-openapi-starter-webmvc-ui。Spring Boot 2.6+ 之后 springfox 兼容性问题频发,springdoc 逐渐成为主流。它的 UI 路径是/swagger-ui/index.html,元数据端点是/v3/api-docs。
这两套方案的共同点是:只要依赖在 classpath 里,且没有显式关闭,Spring Boot 启动时就会自动注册一堆 HTTP 端点。这些端点默认不经过你的业务鉴权逻辑,因为它们是通过独立的Docket(springfox)或GroupedOpenApi(springdoc)配置注册的,走的是 Spring MVC 的RequestMappingHandlerMapping,而不是你自定义的拦截器链——除非你专门把它们也纳入拦截范围。
2.2 自动装配机制:为什么“引了依赖就暴露”
Spring Boot 的核心哲学是“约定优于配置”,自动装配(AutoConfiguration)是它的灵魂。Swagger 的 starter 里通常包含一个spring.factories(Spring Boot 2.7 之前)或AutoConfiguration.imports(2.7 之后)文件,声明了自动配置类。以 springdoc 为例,SpringDocConfiguration会在满足条件时自动创建OpenAPIBean 和相关的HandlerMapping。
关键点在于:这些自动配置类默认是启用的。你只要在pom.xml里加了依赖,哪怕一行 Swagger 配置代码都没写,启动后访问/v3/api-docs也能拿到一份默认的接口描述。很多同学以为“我没写配置类,应该没开吧”,这是典型的误解。实测下来,只要依赖在,端点就在。
2.3 端点暴露的完整清单
不同方案暴露的路径不一样,我整理了一张对照表,方便你排查自己项目里到底开了哪些口子:
| 方案 | UI 页面路径 | 元数据端点 | 常见附加端点 |
|---|---|---|---|
| springfox 2.x | /swagger-ui.html、/swagger-ui/index.html | /v2/api-docs | /swagger-resources、/swagger-resources/configuration/ui |
| springdoc 1.x | /swagger-ui/index.html | /v3/api-docs | /v3/api-docs/swagger-config、/v3/api-docs.yaml |
| springdoc 2.x | /swagger-ui/index.html | /v3/api-docs | 同上,支持按 group 分组/v3/api-docs/{group} |
注意:
/swagger-resources这个端点经常被忽略,它会返回所有 Docket 分组的信息,即使你关了 UI,它也可能还在,等于间接泄露了接口分组结构。
2.4 为什么默认不鉴权:设计初衷与现实的错位
Swagger 的设计初衷是开发协作工具,面向的是内部开发、测试、前端联调场景。在这个语境下,它默认开放是合理的——大家本地跑,谁还去配鉴权。但现实是,很多项目把开发配置直接带到了生产环境,或者用同一套代码部署到公网,于是“内部工具”变成了“公开服务”。
更深层的原因是:Swagger 的端点注册在 Spring MVC 的 HandlerMapping 体系里,而大多数项目的鉴权是通过HandlerInterceptor或 Spring Security 的FilterChain实现的。拦截器的addPathPatterns如果只写了/api/**,那/v3/api-docs自然不在拦截范围内。这不是 Swagger 的锅,是配置边界没划清楚。
3. 未授权访问的真实风险:不只是“看到接口”
3.1 信息泄露:攻击者的侦察地图
最直接的后果是接口结构泄露。一份完整的 OpenAPI 描述里包含:所有路径、HTTP 方法、请求参数名与类型、请求体结构、响应结构、部分注解里的示例值、甚至枚举取值范围。对攻击者来说,这相当于拿到了一份带注释的 API 手册。
我做过一个实验:拿一个中等规模的 Spring Boot 项目(约 80 个接口),只凭/v3/api-docs返回的 JSON,就能推断出业务模块划分、数据库实体字段(因为 DTO 字段名往往和表字段对应)、以及哪些接口是管理类的(路径里带/admin、/manage)。这些信息单独看不算敏感,但组合起来就是精准的攻击情报。
3.2 攻击面扩大:从“盲打”到“精准打击”
没有 Swagger 的时候,攻击者要猜路径、猜参数,成本高。有了 Swagger,他可以直接对着接口列表逐个测试。尤其是以下几类接口,一旦鉴权缺失,后果很严重:
- 用户信息查询接口:
GET /api/user/{id}这类,如果没做越权校验,可以遍历 ID 拿全量用户数据。 - 文件上传/下载接口:路径和参数一目了然,可能被用来上传恶意文件或下载敏感文件。
- 管理后台接口:有些项目管理接口和业务接口在同一个应用里,Swagger 会把它们全列出来。
- 调试/测试接口:开发阶段留下的
/test、/debug接口,忘了删,Swagger 一暴露就全露了。
3.3 结合其他漏洞的“化学反应”
单独一个 Swagger 未授权,可能只是中低危。但它经常和其他问题叠加:
- 配合未授权接口:Swagger 告诉你有哪些接口,其中某个接口恰好没配鉴权,直接打通。
- 配合参数注入:知道了参数名和类型,SQL 注入、命令注入的测试效率大幅提升。
- 配合若依等框架的已知问题:热词里提到“若依 微服务 使用 swagger”,若依早期版本确实存在 Swagger 相关配置问题,攻击者熟悉框架结构后,利用成本更低。
3.4 合规视角:为什么安全扫描总报这个
很多安全扫描器(比如做“原理扫描”的那类工具)会把 Swagger 未授权访问列为中危或高危。原因很简单:它属于可被远程未授权访问的敏感信息端点。在等保、ISO 27001 这类合规检查里,接口文档暴露在公网通常会被判定为不符合“最小暴露原则”。所以不管从技术还是合规角度,上线前关掉它都是必要动作。
4. 检测与验证:怎么确认自己的项目有没有问题
4.1 手工快速验证:三条命令搞定
最直接的方法就是访问那几个已知路径。你可以用浏览器,也可以用 curl:
# springdoc 元数据 curl -s http://your-host:port/v3/api-docs | head -c 500 # springfox 元数据 curl -s http://your-host:port/v2/api-docs | head -c 500 # UI 页面 curl -s -o /dev/null -w "%{http_code}" http://your-host:port/swagger-ui/index.html如果返回 200 且内容里有"openapi"或"swagger"字段,基本可以确认暴露了。返回 401/403 说明有鉴权拦截,返回 404 说明没开或路径不对。
4.2 自动化扫描思路:批量排查多个环境
如果你负责多个项目,手工一个个试太慢。可以写个简单的脚本,把常见路径列出来批量探测:
import requests paths = [ "/v3/api-docs", "/v2/api-docs", "/swagger-ui/index.html", "/swagger-ui.html", "/swagger-resources", "/v3/api-docs/swagger-config", ] targets = ["http://host1:8080", "http://host2:8080"] for target in targets: for path in paths: url = target + path try: r = requests.get(url, timeout=5, allow_redirects=False) if r.status_code == 200 and ("openapi" in r.text or "swagger" in r.text.lower()): print(f"[暴露] {url}") except Exception: pass这个脚本只是演示思路,实际用的时候注意加并发控制和超时,别把目标打挂了。
4.3 从代码层面自查:依赖与配置双检查
光测端点还不够,最好从代码层面确认。检查两处:
pom.xml或build.gradle:搜springfox、springdoc、swagger关键字,确认是否引入了依赖。- 配置类:搜
@EnableSwagger2、@EnableOpenApi、Docket、GroupedOpenApi,看有没有显式配置。如果有,看它的enable()或条件注解是否和生产环境绑定。
实操心得:我见过一个项目,
pom.xml里依赖是<scope>provided</scope>,以为不会打包进去,结果部署时容器里恰好有那个 jar,还是暴露了。所以 scope 不是万能的,最终以运行时 classpath 为准。
4.4 常见误判:这些情况别当成漏洞
- 返回 401/403:说明有鉴权,不算未授权访问,但要确认鉴权是否可绕过。
- 返回 200 但内容是空 JSON:可能是配置了分组但没扫描到接口,风险较低,但仍建议关闭。
- 内网环境:如果确认只在隔离内网、无外部访问路径,风险等级可以下调,但合规上仍建议生产关闭。
5. 修复方案:从“临时止血”到“根治”
5.1 方案一:生产环境直接关闭(推荐)
最彻底的做法是生产环境不启用 Swagger。有两种实现方式:
方式 A:用 Profile 控制依赖生效范围
在pom.xml里把 Swagger 依赖限定在非生产 Profile:
<profiles> <profile> <id>dev</id> <dependencies> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.3.0</version> </dependency> </dependencies> </profile> </profiles>打包时用mvn package -Pdev开发包带 Swagger,生产包用默认 Profile 不带。这样生产环境的 jar 里根本没有 Swagger 的类,端点自然不存在。
方式 B:用配置开关控制
springdoc 支持通过配置关闭:
# application-prod.properties springdoc.api-docs.enabled=false springdoc.swagger-ui.enabled=falsespringfox 则通过 Docket 的enable(false)或配置项springfox.documentation.enabled=false控制。
注意:方式 B 只是不注册端点,但依赖和类还在 classpath 里。如果存在其他绕过方式(比如某些版本的条件判断缺陷),理论上仍有风险。所以安全要求高的场景,优先用方式 A。
5.2 方案二:加鉴权拦截(适合必须保留的场景)
有些团队确实需要生产环境保留 Swagger(比如给合作方看接口),那就必须加鉴权。核心思路是把 Swagger 端点纳入你的鉴权体系。
如果是 Spring Security,可以这样配:
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").hasRole("ADMIN") .anyRequest().authenticated() ); return http.build(); } }如果是自定义拦截器,记得把路径加进去:
registry.addInterceptor(authInterceptor) .addPathPatterns("/**") .excludePathPatterns("/login", "/public/**");实操心得:用
addPathPatterns("/**")全拦截,再排除白名单,比只拦截/api/**更安全。因为后者容易漏掉 Swagger、Actuator 等非业务端点。热词里提到的 “metrics 未授权访问漏洞”“nacos namespaces 未授权访问漏洞” 本质上是同一类问题——端点没纳入统一鉴权。
5.3 方案三:网关层拦截(微服务场景)
微服务架构下,Swagger 可能分散在各个服务里。这时候在网关(如 Spring Cloud Gateway)统一拦截更高效:
spring: cloud: gateway: routes: - id: block-swagger uri: no://op predicates: - Path=/v3/api-docs/**,/swagger-ui/**,/v2/api-docs/** filters: - SetStatus=404这段配置的意思是:匹配到 Swagger 路径的请求,直接返回 404,不转发到后端服务。这样即使某个服务忘了关,网关层也兜住了。
5.4 方案四:自定义路径 + 随机化(不推荐作为唯一手段)
有人会把 Swagger 路径改成随机字符串,靠“隐藏”来防护。这属于安全 through obscurity,只能提高一点门槛,不能作为唯一措施。因为路径可能通过前端 JS、日志、错误信息泄露。如果要用,也必须配合鉴权。
5.5 修复方案对比与选型建议
| 方案 | 彻底性 | 实施成本 | 适用场景 |
|---|---|---|---|
| Profile 隔离依赖 | 高 | 中 | 生产不需要 Swagger |
| 配置开关关闭 | 中 | 低 | 快速止血 |
| 鉴权拦截 | 高 | 中 | 生产需保留 Swagger |
| 网关拦截 | 高 | 中 | 微服务架构 |
| 路径随机化 | 低 | 低 | 仅作辅助 |
我的建议是:生产环境优先用 Profile 隔离,从依赖层面根除;如果必须保留,用鉴权拦截 + 网关兜底双保险。
6. 修复过程中的坑与排查实录
6.1 关了 UI 但元数据还在
这是最常见的坑。很多人只关了swagger-ui,忘了api-docs。结果 UI 页面 404 了,但/v3/api-docs还能返回完整 JSON。修复时要成对关闭:
springdoc.api-docs.enabled=false springdoc.swagger-ui.enabled=false6.2 配置了enabled=false但端点仍可访问
原因可能是配置没生效,常见于:
- 配置文件没被加载(Profile 不对、文件名不对)。
- 有多个配置源冲突(比如 Nacos 配置中心覆盖了本地配置)。
- 版本差异导致配置项名称不同(springdoc 1.x 和 2.x 有细微差别)。
排查方法:启动时看日志里有没有springdoc相关的自动配置报告,或者用/actuator/env看实际生效的配置值。
6.3 Spring Security 放行了却还是 401
有时候配了permitAll但访问还是 401,可能是:
- 请求被 CSRF 拦截了(GET 一般不会,但某些配置下会)。
- 路径匹配写错了,比如
/swagger-ui/**没覆盖/swagger-ui/index.html(实际上/**是覆盖的,但有人写成/swagger-ui/*就只匹配一层)。 - 有多个
SecurityFilterChain,优先级搞错了。
6.4 微服务下每个服务都要改,容易漏
微服务项目里,Swagger 配置往往散落在各个服务的application.yml里。建议:
- 把 Swagger 配置抽到公共依赖或配置中心,统一管理。
- 在 CI/CD 流水线里加一步检查:打包后扫描 jar 里有没有 Swagger 相关类,或者启动后自动探测端点。
- 网关层做统一拦截作为最后防线。
6.5 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| UI 404 但 api-docs 200 | 只关了 UI | 检查两个开关 |
| 配置关闭无效 | 配置未加载/被覆盖 | 看 actuator/env |
| 加了鉴权仍可访问 | 路径未纳入拦截 | 检查拦截器路径匹配 |
| 网关拦截不生效 | 路由优先级问题 | 调整路由顺序 |
| 生产包仍有 Swagger | 依赖未隔离 | 检查打包 Profile |
独家避坑技巧:上线前用
unzip -l your-app.jar | grep -i swagger检查 jar 里有没有 Swagger 相关类。如果有,说明依赖没隔离干净,需要回到 pom 层面处理。
7. 上线前的安全检查清单与长期防护
7.1 上线前必做的五项检查
- 依赖检查:确认生产包不含 Swagger 依赖,或已通过配置关闭。
- 端点探测:用 curl 或脚本探测
/v3/api-docs、/v2/api-docs、/swagger-ui/index.html等路径,确认返回 404 或 401。 - 鉴权覆盖:确认所有非业务端点(Swagger、Actuator、Druid 监控页等)都纳入了鉴权或已关闭。
- 网关规则:微服务场景确认网关层有兜底拦截规则。
- 日志检查:启动日志里不应出现 Swagger 相关的端点注册信息。
7.2 把检查自动化:CI/CD 集成
手工检查容易漏,建议集成到流水线。比如在部署后的冒烟测试阶段加一段:
#!/bin/bash HOST=$1 for path in /v3/api-docs /v2/api-docs /swagger-ui/index.html; do code=$(curl -s -o /dev/null -w "%{http_code}" "$HOST$path") if [ "$code" = "200" ]; then echo "安全检查失败:$path 返回 200" exit 1 fi done echo "安全检查通过"这样每次上线自动跑一遍,有问题直接阻断发布。
7.3 长期防护:建立端点资产清单
Swagger 只是众多“非业务端点”中的一类。同类问题还有 Actuator 的/actuator/env、/actuator/heapdump,Druid 的/druid/index.html,Nacos 的命名空间接口等。建议团队建立一份端点资产清单,记录每个端点的用途、是否鉴权、生产是否开启。新引入依赖时,先查它会不会自动注册端点,再决定怎么处理。
7.4 版本升级的注意事项
springdoc 和 springfox 的版本更新有时会改变默认行为。比如某些版本默认关闭了某些端点,某些版本又调整了路径。升级依赖后,务必重新跑一遍端点探测。另外,Spring Boot 2.6+ 对路径匹配策略有调整(PathPatternParser),可能影响拦截器匹配结果,升级时也要注意。
7.5 团队协作层面的建议
技术手段之外,流程也很重要。我的经验是:
- 在代码模板或脚手架里,默认就把 Swagger 配成“仅 dev 启用”。
- Code Review 时把“生产配置是否关闭调试端点”列为检查项。
- 安全扫描工具接入 CI,把 Swagger 未授权列为阻断项。
这样即使个别同学疏忽,流程也能兜住。
8. 写在最后:一点个人体会
做安全这些年,我发现一个规律:大部分严重问题,都不是因为用了多高深的技术,而是因为基础配置没做对。Swagger 未授权访问就是典型——它不涉及复杂的漏洞利用,就是一个“该关的没关”。但恰恰是这种“简单”的问题,最容易在赶工期、多环境切换、微服务拆分的过程中被忽略。
我自己踩过的坑是:早期做项目时,觉得“内网环境无所谓”,结果有一次内网被横向渗透,攻击者就是从一台机器的 Swagger 页面开始,摸清了整个内网的接口结构。从那以后,我养成了一个习惯:不管什么环境,调试类端点一律默认关闭,需要时再按需开启,并且开启时必须加鉴权。这个习惯帮我省了很多事后排查的麻烦。
如果你现在正在维护一个 Spring Boot 项目,建议花十分钟按上面的清单过一遍。尤其是那些部署在公网、或者虽然在内网但有多人共用的环境,别让一份接口文档成了别人进入你系统的第一把钥匙。