资讯动态

Spring Boot Swagger未授权访问:风险与修复方案

发布时间:2026/9/20 14:37:36 来源:尧图企业网站定制
1. 从一个被扫出来的接口文档说起很多团队在项目上线前的安全扫描环节都会收到一条看起来不痛不痒的告警某个路径下存在接口文档页面可未授权直接访问。扫描器给出的风险等级往往不高于是被顺手标记成误报或者下个迭代再处理。直到某天有人发现攻击者根本不需要猜接口路径直接打开文档页面就能拿到全部接口清单、参数结构、字段含义甚至连内部调试接口和管理接口都一览无余。这就是Spring Boot 项目中 Swagger 未授权访问的典型场景。它本身不是一个漏洞级别的代码缺陷而是一个配置疏忽导致的信息暴露面。但正是这种看起来不严重的问题往往成为后续横向渗透的起点。接口文档里暴露的每一个路径、每一个参数名都是攻击者构造请求的现成素材。这篇内容面向的是正在使用 Spring Boot 做后端开发、并且集成了 Swagger或 springdoc、Knife4j 等同类接口文档工具的工程师。我会把这件事拆成几块讲清楚Swagger 在 Spring Boot 里到底是怎么被暴露出去的、为什么默认配置下它几乎必然可未授权访问、真实风险边界在哪里、以及几种不同场景下可落地的修复方案。中间会穿插我自己在项目里踩过的坑包括那些改了配置但没生效的典型情况。需要先说明一点本文讨论的所有内容都限定在自有系统的安全加固范围内目的是帮助开发和运维人员把自家服务的暴露面收敛掉不涉及任何针对他人系统的操作。2. Swagger 在 Spring Boot 里是怎么被挂出去的2.1 自动装配带来的默认暴露Spring Boot 的核心设计哲学是约定优于配置大量功能通过 starter 自动装配完成。Swagger 的集成同样如此。以 springfox 为例只要在pom.xml里引入springfox-boot-starter再写一个Configuration类加上EnableSwagger2或EnableOpenApi一个完整的文档站点就自动挂载好了。关键在于这个文档站点默认注册在 Spring MVC 的 DispatcherServlet 上和业务接口共享同一套请求处理链路。也就是说只要应用端口对外可达文档页面就对外可达。它不会因为你没在 Controller 里写映射就消失因为它是通过Docket配置动态生成的。springdoc-openapi 的逻辑类似。引入springdoc-openapi-ui之后默认会暴露这几个路径路径作用/swagger-ui.htmlUI 入口通常 302 跳转到下面的 index/swagger-ui/index.html实际的文档界面/v3/api-docsOpenAPI 3.0 规范的 JSON 描述/v3/api-docs/swagger-configUI 的配置信息/swagger-resourcesspringfox 时代的资源列表这些路径是框架写死的默认值不需要开发者做任何额外声明。很多人以为我没主动开放它但事实是我没主动关闭它。2.2 为什么默认没有鉴权这是最容易被误解的一点。Swagger 的 UI 和 api-docs 端点本质上就是普通的 HTTP 资源它们不经过你的业务鉴权逻辑。原因有两层第一层大多数项目的鉴权是通过拦截器Interceptor或过滤器Filter实现的而这些组件的放行规则里通常会包含静态资源路径。开发者为了让前端页面、图标、JS 文件能正常加载往往写的是放行/swagger-ui/**、/v3/api-docs/**这类规则。一旦放行鉴权就绕过了。第二层如果你用的是 Spring Security默认配置下所有请求都需要认证但很多教程为了先跑起来会直接写http.authorizeRequests().antMatchers(/**).permitAll()或者干脆把 Swagger 相关路径加进白名单。这两种做法都会让文档端点变成匿名可访问。我在一个真实项目里见过更隐蔽的情况项目用了自定义的网关鉴权网关层只校验了业务前缀的路径而 Swagger 的路径不在校验范围内于是直接从网关透传到了后端服务。这种鉴权在网关、暴露在后端的错位是很多微服务架构下的通病。2.3 生产环境打包时它并不会自动消失一个常见的侥幸心理是这是开发环境的东西打包上线应该就没了。事实并非如此。Swagger 的依赖是编译期依赖配置类也是普通 Bean只要在 classpath 里、只要 profile 没做区分它就会在任何环境下生效包括生产环境。我见过有团队用Profile(dev)标注 Swagger 配置类这确实是一种正确做法。但问题在于很多项目的 profile 激活方式是spring.profiles.activedev写死在配置文件里上线时忘了改或者运维用环境变量覆盖时漏了这一项结果生产环境照样把文档暴露出去。3. 未授权访问的真实风险边界3.1 信息泄露是第一层也是最直接的一层打开一个未授权的 Swagger 页面攻击者能拿到什么远不止接口列表这么简单。全部接口路径和 HTTP 方法包括那些没有在前端使用的内部接口、管理接口、调试接口。请求参数结构字段名、类型、是否必填、示例值。字段名往往能透露业务含义比如isAdmin、internalToken、userId。响应结构返回字段同样暴露业务模型。接口分组和描述很多团队会在ApiOperation里写详细的中文说明等于把接口说明书直接送出去。部分框架还会暴露实体类的字段注释进一步降低攻击者的理解成本。把这些信息拼起来攻击者几乎不需要逆向就能理解你的业务模型。这比盲扫目录高效得多。3.2 从信息泄露到实际攻击的链路信息本身不是终点。真正的风险在于这些信息会显著降低后续攻击的成本。举几个我在安全评估中见过的真实链路第一种文档里暴露了一个/api/internal/user/export接口参数是userId。攻击者发现这个接口没有做权限校验因为原本设计是内部调用于是遍历 userId 批量导出用户数据。第二种文档里暴露了某个接口的debug参数默认 false但传 true 时会返回详细的异常堆栈。攻击者借此拿到数据库表名和 SQL 片段。第三种文档暴露了文件上传接口的完整参数包括存储路径字段。攻击者构造路径穿越的请求把文件写到非预期目录。这些都不是 Swagger 本身的漏洞而是Swagger 把攻击面完整地展示了出来。没有文档攻击者需要花大量时间盲测有了文档这些接口就像被贴上了标签。3.3 为什么扫描器给的等级往往偏低安全扫描器通常把未授权访问接口文档归类为信息泄露风险等级中等或偏低。这个定级逻辑本身没错因为单看这一个点它不直接导致数据被篡改或泄露。但定级偏低带来的副作用是团队容易忽视它。我的建议是不要只看扫描器的等级要看这个文档暴露在什么网络位置。如果服务只在内网、且有严格的网络隔离风险相对可控如果服务对公网可达那这个问题的优先级应该直接拉高。判断标准很简单把文档页面的 URL 拿到公网环境访问一下能打开就是高危。4. 修复方案从关掉到管住的几种思路4.1 方案一生产环境彻底关闭文档最直接的做法就是让 Swagger 在生产环境不生效。这里的关键是用 profile 做隔离并且确保隔离真的生效。以 springdoc 为例可以这样配置# application.yml springdoc: api-docs: enabled: false swagger-ui: enabled: false然后在开发环境用application-dev.yml覆盖为 true。但这里有个坑springdoc.api-docs.enabledfalse只关闭了 api-docs 端点UI 页面可能仍然可访问取决于版本。更稳妥的做法是配合 profile 条件装配配置类Configuration Profile({dev, test}) public class SwaggerConfig { Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info().title(项目接口文档).version(1.0)); } }这样在非 dev/test 环境下这个配置类根本不会被加载文档自然不存在。注意用Profile隔离时一定要确认生产环境的spring.profiles.active不是 dev。我建议在启动脚本里加一行日志把当前激活的 profile 打出来上线时肉眼确认一遍。4.2 方案二保留文档但加鉴权有些团队确实需要生产环境的文档比如给合作方对接用这时候就不能简单关掉而是要做访问控制。如果项目用了 Spring Security可以把 Swagger 路径纳入认证范围Configuration public class SecurityConfig { Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers(/swagger-ui/**, /v3/api-docs/**).authenticated() .anyRequest().permitAll() .and() .httpBasic(); return http.build(); } }这样访问文档需要先通过 HTTP Basic 认证。但要注意Basic 认证的凭据是明文传输的Base64 不是加密所以必须配合 HTTPS 使用。如果项目用的是自定义拦截器思路类似把 Swagger 路径从白名单里移除让它走正常的登录校验。这里有个细节Swagger UI 加载时会请求多个资源JS、CSS、api-docs如果只拦截了入口页面而没拦截 api-docs攻击者仍然可以直接请求/v3/api-docs拿到接口 JSON。所以鉴权规则要覆盖所有相关路径不能只拦 UI。4.3 方案三网关层统一拦截在微服务架构下更推荐的做法是在网关层做统一处理。因为后端服务可能有几十个逐个改配置容易遗漏而网关是所有流量的入口。以 Spring Cloud Gateway 为例可以加一个全局过滤器对 Swagger 相关路径做拦截Component public class SwaggerBlockFilter implements GlobalFilter, Ordered { private static final ListString BLOCK_PATHS Arrays.asList( /swagger-ui, /v3/api-docs, /swagger-resources, /webjars ); Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); for (String block : BLOCK_PATHS) { if (path.contains(block)) { exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND); return exchange.getResponse().setComplete(); } } return chain.filter(exchange); } Override public int getOrder() { return -100; } }这个过滤器的逻辑是只要路径里包含 Swagger 相关关键字直接返回 404。返回 404 而不是 403是为了不暴露这里本来有东西的信息。提示网关拦截要注意路径匹配的准确性。有些项目的业务接口路径里恰好包含swagger字样虽然少见用contains可能误伤。更严谨的做法是用startsWith配合路径前缀列表。4.4 方案四改掉默认路径如果既想保留文档又不想被扫描器轻易发现可以修改默认路径。springdoc 支持自定义springdoc: swagger-ui: path: /doc-${random.uuid} api-docs: path: /api-spec-${random.uuid}用随机值做路径扫描器基于默认字典就扫不到了。但我要强调这只是隐藏不是防护。一旦路径被泄露比如前端代码里引用了、日志里打印了防护就失效了。所以这个方案只能作为辅助手段不能替代鉴权。5. 那些改了配置却没生效的排查过程5.1 依赖冲突导致配置类没加载有一次我在一个老项目里改 Swagger 配置明明加了Profile(dev)生产环境却依然能访问文档。排查了半天最后发现项目里同时存在 springfox 和 springdoc 两套依赖。我改的是 springdoc 的配置但实际生效的是 springfox 的自动配置。排查方法很简单启动时看日志里有没有DocumentationPluginsBootstrapper或OpenApiResource相关的初始化信息。或者直接看/swagger-resources这个端点是否存在——它是 springfox 特有的springdoc 不用这个路径。经验接手一个项目要改 Swagger 配置前先确认用的是哪套工具。看pom.xml里是springfox-boot-starter还是springdoc-openapi-ui两者的配置项完全不同混用会出各种奇怪问题。5.2 静态资源放行规则覆盖了鉴权另一个高频坑是鉴权配置里明明写了要拦截 Swagger 路径但实际还是能访问。原因通常是放行规则和拦截规则的顺序问题。在 Spring Security 的链式配置里规则是从上到下匹配匹配到就停止。如果前面有一条.antMatchers(/**).permitAll()后面的 Swagger 拦截规则就永远不会生效。正确的顺序是把具体路径的规则放在前面http.authorizeRequests() .antMatchers(/swagger-ui/**).authenticated() // 具体规则在前 .antMatchers(/v3/api-docs/**).authenticated() .anyRequest().permitAll(); // 兜底规则在后这个顺序问题在自定义拦截器里同样存在。我见过有项目在WebMvcConfigurer里注册了多个拦截器Swagger 的放行逻辑写在了一个excludePathPatterns里而另一个拦截器又把它加回来了导致最终行为取决于拦截器的注册顺序。5.3 缓存和 CDN 让关闭看起来没生效还有一种情况配置改对了服务也重启了但浏览器访问文档页面还是能打开。这通常是浏览器缓存或 CDN 缓存导致的。Swagger UI 的静态资源JS、CSS会被浏览器强缓存即使后端已经返回 404页面可能还在用缓存渲染。排查方法用无痕窗口访问或者用curl直接请求接口curl -i http://your-host/v3/api-docs如果curl返回 404 而浏览器能打开那就是缓存问题。清理缓存或加版本号即可。5.4 多实例部署下的配置不一致在容器化部署的环境里服务可能有多个实例。如果配置是通过环境变量注入的而某个实例的环境变量没更新就会出现部分实例关闭了、部分实例还开着的情况。扫描器只要扫到任意一个实例就会报出问题。排查方法逐个实例请求文档路径确认所有实例的行为一致。这个检查在滚动发布之后尤其重要。6. 把这件事做成一个可持续的检查项6.1 上线前的自检清单与其每次靠扫描器提醒不如把检查固化到流程里。我整理了一份自检清单上线前逐项确认检查项确认方式通过标准文档端点是否可匿名访问无痕窗口访问/swagger-ui/index.html返回 401/403/404api-docs 是否可匿名访问curl请求/v3/api-docs返回 401/403/404生产环境 profile 是否正确查看启动日志中的 active profile不是 dev/test网关是否拦截了文档路径从网关入口访问文档路径返回 404所有实例行为是否一致逐个实例请求全部不可访问这份清单看起来简单但能覆盖绝大多数遗漏场景。我建议把它写进 CI 流程用脚本自动跑一遍。6.2 用自动化脚本做回归检查手动检查容易忘可以写一个简单的脚本在每次发布后自动验证#!/bin/bash HOST$1 PATHS(/swagger-ui/index.html /v3/api-docs /swagger-resources) for path in ${PATHS[]}; do code$(curl -s -o /dev/null -w %{http_code} http://${HOST}${path}) if [ $code 200 ]; then echo [FAIL] ${path} 返回 200存在未授权访问风险 else echo [PASS] ${path} 返回 ${code} fi done把这个脚本挂到发布流水线的最后一步一旦有实例返回 200 就中断发布。这比事后被扫描器通报要主动得多。6.3 关于要不要保留生产文档的取舍最后聊一个决策层面的问题生产环境到底要不要保留接口文档我的观点是默认关闭确有需要再按最小权限开放。如果确实要给外部对接方提供文档更好的做法是单独导出一份静态文档比如用 springdoc 的离线导出功能生成 HTML 或 PDF通过受控的渠道分发而不是把在线文档直接暴露出去。在线文档的价值在于实时更新但生产环境的接口变更频率通常不高静态文档完全够用。用静态文档替代在线文档既满足了对接需求又消除了未授权访问的风险是一笔划算的买卖。如果团队坚持要保留在线文档那至少要做到三点走 HTTPS、加认证、限制来源 IP。这三条缺一不可。我在实际项目里见过只加了认证但没限制 IP 的结果认证凭据被弱口令爆破文档照样泄露。7. 一个容易被忽略的关联点其他同类端点Swagger 不是唯一会被默认暴露的端点。Spring Boot Actuator 的/actuator路径下/actuator/env、/actuator/health、/actuator/metrics等端点同样可能未授权访问。其中/actuator/env会暴露环境变量风险比 Swagger 更高。所以做安全加固时建议把这类框架自带的、默认开放的端点统一梳理一遍。判断方法很简单翻一遍项目引入的 starter看看哪些会注册额外的 HTTP 端点。常见的包括springdoc / springfox接口文档spring-boot-starter-actuator监控端点H2 Console数据库控制台/h2-consoleDruid监控页面/druid/**这些端点的加固思路和 Swagger 一致要么关闭要么鉴权要么在网关拦截。把它们列成一张清单逐个确认比零散处理要可靠得多。我在一个项目里做过统计光是框架默认暴露的端点就有七八个其中三个是未授权可访问的。这些问题单看都不严重但叠加起来攻击者能拼凑出的信息量相当可观。安全这件事往往就是把这些小问题一个个收掉整体暴露面才会真正降下来。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价