Swagger,绝大多数后端开发者都听过、也用过,但很多人对它的了解停留在"加个依赖、启动项目、打开一个网页"这个层面。中文圈还给Swagger起了个特别接地气的名字——丝袜哥。这个谐音虽然有点搞笑,但只要你在做前后端分离、写开放接口或者搞微服务,不管是Java、Python还是Go,迟早都要跟这个"丝袜哥"打交道。
这篇文章基于我自己这几年在多个项目里实际使用Swagger的经验,从零讲清楚三件事:Swagger到底是什么工具、怎么在Spring Boot里快速跑起来、以及生产环境里最常见的未授权访问漏洞是怎么一回事。另外会单独用一节讲Python生态和微服务场景下的玩法,因为这两个方向问的人实在太多了。文章里所有代码示例都是能直接跑的,版本兼容这些坑我也会专门列一节,照着做基本不会翻车。
1. "丝袜哥"这名字背后的东西:Swagger到底是什么工具
1.1 从一家公司的内部工具到行业规范
Swagger最早是2011年左右由一家叫Wordnik的公司内部开发的API文档工具,后来开源出来,再后来由SmartBear公司接手维护,并且在2015年把这个规范捐给了Linux基金会下面的OpenAPI Initiative组织,从此改名为OpenAPI Specification,简称OAS。
注意这个命名变化很重要。日常聊天里大家还是习惯叫"Swagger",但严格来说,Swagger现在指的是那一套工具家族,而它定义的接口描述规范叫OpenAPI。你现在看到的各种框架生成的api-docs、openapi.json,本质上都是在输出一份符合OAS规范的结构化数据。理解了这层关系,后面遇到"为什么接口路径是/v3/api-docs""为什么有人把它叫OpenAPI"这些问题就不会懵。
简单来说,Swagger解决的是接口文档的生产、展示和调用问题。它干的事情可以用一句话概括:把接口信息(路径、参数、返回值、鉴权方式)用结构化的JSON描述出来,然后在一个网页上渲染成人类可读的文档,并且支持直接在网页上发起请求调试接口。
1.2 Swagger工具家族与周边生态
很多人以为Swagger就等于那个绿底黑字的网页,其实那只是其中一屏。整个生态里最常见的是这几块:
- Swagger UI:就是把JSON渲染成网页的那个东西,也是大家日常见得最多的界面。它最实用的功能是每个接口右侧都有"Try it out"按钮,可以直接填参数、发起真实请求,不用再打开Postman。
- Swagger Editor:一个基于浏览器的编辑器,用YAML或JSON写OpenAPI定义,左边写右边立即渲染出文档。适合从零手写规范做设计,不过国内项目直接用代码注解生成的居多。
- Swagger Codegen / OpenAPI Generator:根据接口定义自动生成客户端SDK、服务端代码的脚手架工具。工具虽好,但生成的代码风格未必符合团队规范,实际项目里用得不多,更多是拿来生成给前端调用的类型定义。
- Knife4j:国内开发者基于Spring Boot对Swagger UI做的增强版,文档首页叫
doc.html,界面更符合国内使用习惯,对Spring Cloud微服务聚合场景支持得特别好,后面我会单独讲。
从技术栈来看,Java生态里有两代主流实现:老一代是springfox,新一代是springdoc-openapi。springfox在2020年更新完3.0.0版本后基本停更了,对Spring Boot 2.6以上版本会出现启动报错;springdoc现在是事实上的标准选择,而且直接支持OpenAPI 3规范。我接手的老项目还在用springfox,新项目一律springdoc。
1.3 它到底解决了什么问题
最直接的回答是:解决了接口文档跟不上代码的问题。我见过太多项目,文档停留在上上个版本,前端同事照着文档对接接口,调了半天发现字段名早改了,气得在群里@后端。有了Swagger之后,文档从代码注释里生成,代码变了文档就变,至少不会出现"文档说的是A,代码跑的是B"的错位。
另一个不那么明显但很重要的价值是:Swagger把接口变成了一种可以"执行"的文档。打开页面的"Try it out",填参数、点执行,就能看到真实响应。这比把接口描述发给前端、让前端自己猜要高效得多,联调阶段省下的沟通成本非常可观。
2. 十五分钟跑通Spring Boot接入:选型、依赖和第一个接口文档
2.1 选型:老掉牙的springfox就别再用了
先给结论:Spring Boot 2.x请用springdoc-openapi-ui 1.7.0,Spring Boot 3.x请用springdoc-openapi-starter-webmvc-ui 2.x以上版本。
springfox之所以被淘汰,除了停更之外,还有个致命的问题是它内部使用了旧版本的guava和swagger-models,跟Spring Boot 2.6之后引入的pathmatch策略变更直接冲突。典型报错是:
Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException看到这个报错,网上老教程会告诉你加一行spring.mvc.pathmatch.matching-strategy=ant_path_matcher,这确实是springfox的临时解药,但属于治标不治本。与其加配置硬撑,不如直接迁到springdoc,注解迁移成本其实很低。
2.2 完整接入步骤:依赖、配置类、启动验证
以最常见的Spring Boot 3.x + Maven项目为例,pom.xml加一个依赖就够:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.2.0</version> </dependency>接着写一个配置类,把文档的基础信息定义好:
import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Info; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class OpenApiConfig { @Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info() .title("用户服务 API") .version("1.0.0") .description("用户服务对外接口文档,包含用户查询、创建、删除等接口。")); } }就这两步。启动Spring Boot应用,浏览器访问下面任意一个地址:
http://localhost:8080/swagger-ui/index.html—— Swagger UI页面http://localhost:8080/v3/api-docs—— OpenAPI定义的原始JSON
这两个地址的关系可以这样理解:api-docs是数据源,swagger-ui是把这个JSON渲染出来的网页。如果打开的页面是空的,第一步去访问/v3/api-docs看有没有JSON返回,有说明数据没问题,是UI加载的问题;没有则是注解或配置没生效。这个排查方向能省很多时间。
Spring Boot 2.x的话依赖改成org.springdoc:springdoc-openapi-ui:1.7.0,访问地址是/swagger-ui.html,实际会重定向到/swagger-ui/index.html,其他配置逻辑完全一样。
2.3 分组与多模块项目的配置思路
项目大了之后,一个服务里可能挂着多个业务模块,比如用户模块、订单模块、支付模块。不分组的后果是:打开页面后几百个接口混在一起,前端光找接口就找半天。springdoc支持按包路径分组:
@Bean public GroupedOpenApi userApi() { return GroupedOpenApi.builder() .group("用户模块") .packagesToScan("com.example.controller.user") .build(); } @Bean public GroupedOpenApi orderApi() { return GroupedOpenApi.builder() .group("订单模块") .packagesToScan("com.example.controller.order") .build(); }新增这个之后,Swagger UI左上角会出现下拉框切换组。分组配置算是Spring Boot项目里Swagger使用体验提升最大、成本最低的一步,建议从一开始就做,不要等接口攒到几百个再回头拆。
3. 把接口文档写出人味:核心注解与描述规范
3.1 springdoc与springfox注解对照
很多从老项目迁过来的同学,最头疼的是注解全变了。其实对照关系很简单,我直接列个对照表:
| 用途 | springfox(旧) | springdoc(新) |
|---|---|---|
| Controller类说明 | @Api(tags = "用户管理") | @Tag(name = "用户管理", description = "用户相关接口") |
| 接口方法说明 | @ApiOperation("获取用户信息") | @Operation(summary = "获取用户信息", description = "根据ID获取用户详细信息") |
| 参数说明 | @ApiParam("用户ID") | @Parameter(description = "用户ID") |
| 实体类说明 | @ApiModel("用户实体") | @Schema(description = "用户实体") |
| 字段说明 | @ApiModelProperty("用户名") | @Schema(description = "用户名") |
大部分情况下,改注解是纯机械操作,不会动业务逻辑。迁完记得跑一遍接口测试,重点看文档里的参数是否齐全、返回结构是否正确。
3.2 Controller和实体类的标准写法
我这里给一套我自己项目里常用的标准写法,直接照着套就行。先看Controller层:
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.web.bind.annotation.*; @Tag(name = "用户管理", description = "用户的查询、创建与删除") @RestController @RequestMapping("/api/users") public class UserController { @Operation(summary = "查询用户详情", description = "根据用户ID查询用户基本信息,用户不存在时返回404") @GetMapping("/{userId}") public UserVO getUser( @Parameter(description = "用户ID,正整数", example = "1001") @PathVariable("userId") Long userId) { return userService.getById(userId); } @Operation(summary = "创建用户") @PostMapping public UserVO createUser(@RequestBody UserCreateDTO dto) { return userService.create(dto); } }再看实体类(DTO/VO):
import io.swagger.v3.oas.annotations.media.Schema; @Schema(description = "创建用户请求参数") public class UserCreateDTO { @Schema(description = "用户名,3-20个字符", example = "zhangsan", requiredMode = Schema.RequiredMode.REQUIRED) private String username; @Schema(description = "邮箱地址", example = "zhangsan@example.com") private String email; }这里有个细节值得注意:example这个属性很多人不写,但写上之后,Swagger UI的接口调试区域会自动填入示例值,前端联调时点一下"Try it out"就能直接发请求,不用手动一个个填字段。这对提升联调效率特别明显,是我强烈建议养成的习惯。
3.3 全局参数:统一把Token整上
现在接口基本都要鉴权,最常见的就是请求头带一个Authorization: Bearer xxx。如果每个接口都去写一遍@Parameter,又繁琐又容易漏。springdoc支持全局参数定义:
@Bean public OpenAPI customOpenAPI() { return new OpenAPI() .info(new Info().title("用户服务 API").version("1.0.0")) .components(new Components() .addSecuritySchemes("bearerAuth", new SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") .bearerFormat("JWT"))) .addSecurityItem(new SecurityRequirement().addList("bearerAuth")); }配上之后,Swagger UI右上角会出现一个"Authorize"按钮,点进去填一次Token,后续每个接口调试都会自动带上这个请求头。前端拿去对接时,也不用自己在请求头里拼Token了。
4. 未授权访问这个坑:原理、自查与防护
4.1 扫描器为什么会报"Swagger未授权访问漏洞"
很多团队上线前做安全扫描,报告里会出现一条"Swagger API 未授权访问漏洞【原理扫描】【可验证】"。第一次看到的同学可能会愣住:Swagger不是一个开发工具吗,怎么成漏洞了?
问题不在于Swagger本身,而在于它没做任何访问控制就直接暴露到了公网。Swagger的api-docs接口会返回服务里所有接口的定义,包括路径、参数、请求方式,甚至某些情况下响应结构里会泄露数据库字段设计。攻击者拿到这份清单之后,不需要任何猜测,直接对着文档里的接口逐个调用,很容易撞出不带鉴权的管理接口、内部接口。
更麻烦的是,Swagger UI自带"Try it out"功能,相当于给了访问者一个可以免费使用的接口调试台。配合像/actuator这类信息泄露端点,攻击者可以把系统结构摸得一清二楚。这就是为什么安全扫描器会把"Swagger未授权访问"当成中高危漏洞来报。
4.2 自查:三分钟确认你的服务是否裸奔
判断自己负责的服务是不是有这个隐患,方法很简单。先确认Swagger相关端点能否在外网访问,Spring Boot服务常见的有这几类:
- springfox:
/swagger-ui.html、/webjars/**、/v2/api-docs - springdoc:
/swagger-ui/index.html、/swagger-ui/**、/v3/api-docs - Knife4j:
/doc.html、/v3/api-docs
自查命令可以直接用curl看状态码和返回内容:
curl -s -o /dev/null -w "%{http_code}" http://你的服务地址/swagger-ui/index.html curl -s http://你的服务地址/v3/api-docs | head -c 500第一个命令返回200说明UI页面可以访问,第二个命令如果返回了JSON格式的接口列表,说明api-docs也没有任何拦截。两个都能通,基本可以确定你的接口定义对外裸奔了。这一步只建议用来检查自己维护的系统,确认之后立刻补防护,不要拿去做任何未授权的探测。
4.3 防护方案:从环境隔离到接口鉴权
防护手段没有银弹,按实施成本从低到高排,我做了个对比:
| 方案 | 实施成本 | 效果 | 适用阶段 |
|---|---|---|---|
| 生产环境关闭Swagger | 极低 | 彻底不暴露 | 所有项目都应做到 |
| 网络层限制内网访问 | 低 | 挡住外网,防不了内网 | 没有统一鉴权体系时兜底 |
| 接入统一鉴权 | 中 | 真实有效拦截 | 有Spring Security/Gateway的项目 |
| 只读模式或隐藏敏感接口 | 中 | 防调试,不防读取 | 需要对外开放文档的团队 |
生产环境关闭是最基本的一条。最简单的方式是区分环境配置,比如把Swagger依赖声明为runtimeOnly并在配置类上用@Profile限制:
@Configuration @Profile("dev") public class OpenApiConfig { // 配置类内容 }或者更彻底一点,用Maven Profile控制依赖只在开发环境引入:
<profiles> <profile> <id>dev</id> <activation> <activeByDefault>true</activeByDefault> </activation> <dependencies> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <version>2.2.0</version> </dependency> </dependencies> </profile> </profiles>依赖都没打进生产包,物理上就不存在这个端点,扫描器自然扫不到。
接入统一鉴权适用于文档需要对外开放,但只允许授权人员查看的场景。在Spring Security里给Swagger路径加规则即可:
http.authorizeHttpRequests(auth -> auth .requestMatchers("/swagger-ui/**", "/v3/api-docs", "/doc.html").authenticated() .anyRequest().permitAll() );如果公司有统一的SSO或网关鉴权,把Swagger路径的鉴权也收敛到网关层,效果更好。总之思路就一条:文档也是业务资产,不该默认对所有人敞开。
5. 换到Python和微服务场景:Swagger还能这么玩
5.1 Python生态的三个主流方案
Java之外,Python项目的Swagger接入更为省事,因为很多Web框架直接把Swagger UI打包进了内置功能里。
- FastAPI:开箱即用,创建应用时传个标题,
/docs就是Swagger UI,/redoc是ReDoc风格文档,/openapi.json是原始定义。几乎零配置。 - Flask:需要装第三方库
flasgger或flask-swagger-ui。其中flasgger支持在docstring里用YAML写接口描述,不涉及代码侵入。 - Django:老牌方案是
drf-yasg,现在更推荐drf-spectacular,它严格遵循OpenAPI 3规范,生成的schema质量更高。
用FastAPI写起来大概是这种感觉:
from fastapi import FastAPI app = FastAPI( title="用户服务 API", description="Python 版用户服务接口文档", version="1.0.0", ) @app.get("/users/{user_id}", tags=["用户管理"], summary="查询用户详情") def get_user(user_id: int): """根据用户ID查询用户信息""" return {"user_id": user_id, "name": "测试用户"}启动之后访问http://127.0.0.1:8000/docs,页面和Java版的Swagger UI长得几乎一样。FastAPI能自动从Python类型注解推断出参数和返回结构,所以Python项目养文档的成本比Java还要低。
Flask配flasgger稍微绕一点,接口描述写在docstring里:
from flask import Flask, jsonify from flasgger import Swagger app = Flask(__name__) swagger = Swagger(app) @app.route('/users/<int:user_id>', methods=['GET']) def get_user(user_id): """获取用户信息 --- parameters: - name: user_id in: path type: integer required: true description: 用户ID responses: 200: description: 用户信息 schema: type: object properties: user_id: type: integer name: type: string """ return jsonify({"user_id": user_id, "name": "测试用户"})5.2 微服务架构里的Swagger聚合:以若依为例
微服务架构下,每个服务都有自己的Swagger文档。如果前端对接时得记哪个服务在哪个端口、打开哪个页面,就完全失去意义了。于是"聚合文档"成了刚需,把所有微服务的接口定义拉到同一个页面统一展示。
以国内用得非常多的若依微服务脚手架为例,它集成的是Knife4j的网关聚合方案。原理很简单:后端服务各自提供/v3/api-docs,网关启动后通过服务发现拿到所有实例列表,挨个拉取api-docsJSON,在doc.html里按服务分组渲染。
若依微服务版的网关模块里,application.yml核心配置大致是这样:
knife4j: gateway: enabled: true strategy: discover discover: enabled: true version: openapi3配上之后,访问网关的doc.html就能看到所有子服务的接口聚合列表了。需要注意的点是:
- 子服务必须都能通过网关内部网络访问到自己的
/v3/api-docs,注意不要配成外网地址。 version: openapi3对应springdoc,如果是老项目用的springfox(v2规范),改成v2。- 网关聚合拉不到文档时,先从子服务单独访问
/v3/api-docs排查,不要一上来就怀疑网关配置。
5.3 微服务场景下的分组与权限实践
服务多了之后,聚合页面会变得很长。我建议每个服务内部先做好分组,比如按业务模块分,这样聚合到网关后,前端看到的是"服务 -> 模块 -> 接口"三层结构,可读性会好很多。
另外,微服务的鉴权比单体更依赖网关。前面提到的未授权访问漏洞,在微服务体系里影响的不是一个服务,而是整个服务群的接口定义。所以无论是Swagger UI还是api-docs,都建议在网关层加鉴权,而不是指望每个子服务自己处理。子服务一般只暴露给网关调用,Swagger路径即便开放,也仅限于内网,双重保险更稳妥。
6. 高频踩坑清单与我的长期使用习惯
6.1 启动失败与空白页
坑一:Spring Boot 2.6及以上 + springfox报NPE。这个问题前面讲过,根治方案是换springdoc。如果暂时动不了,临时加spring.mvc.pathmatch.matching-strategy=ant_path_matcher也能顶一阵,但别拖太久。
坑二:页面打开但接口列表为空。大概率是Controller没有被扫描到,或者springdoc的packagesToScan配错了路径。对照一下Controller所在的包路径和GroupedOpenApi里的packagesToScan是否一致。另外检查Controller是否真的被Spring容器管理,没有@RestController注解是不会被扫描的。
坑三:返回的JSON里中文乱码。多数情况下是接口返回字符串时未指定produces编码。在Controller或@RequestMapping上加上produces = "application/json;charset=UTF-8"即可,不过现在Spring Boot默认UTF-8,遇到这个问题的概率已经很低了。
6.2 版本兼容排查思路
Swagger相关依赖跟框架版本的绑定性很强,升级框架版本时尤其容易出问题。我自己的排查套路是这样的:
- 先确认Spring Boot主版本,2.x和3.x的springdoc坐标完全不一样。
- 查看
springdoc的版本兼容说明,它一般会在GitHub的README里写上支持哪个Spring Boot版本。 - 升级后重点检查三处:
/v3/api-docs是否返回正常、分组下拉是否生效、try it out请求是否能通。 - 如果项目里同时有老代码在用
@Api注解、新代码用@Tag注解,先看springdoc是否开启了旧注解兼容开关,没开的话老接口会丢失描述。
6.3 一些实际经验
最后分享几个我从项目里总结出来的使用习惯,不一定适合所有团队,但确实帮我省了不少事。
第一,把Swagger配置的开关集中到一个配置类,不要散落在各个Controller里。全局信息、分组、安全Scheme都放一起,环境切换时只用改一个类。
第二,写接口描述时,把"给谁用"想清楚。如果文档是给前端看的,summary就写业务动作,比如"根据手机号查询用户订单列表",别写"queryOrderByMobile"这种代码味很重的描述。描述字段同理,写业务含义而不是字面含义,比如status的可用值范围一定要列出来,否则前端根本不知道传什么。
第三,文档质量要纳入代码评审。我见过太多项目,Swagger接好了,但注解一个都不写,打开页面全是"接口描述:无"。Swagger的价值建立在"有人认真写描述"这个前提下,没人写的话,它跟没有文档的区别并不大。
第四,定期对着线上环境复盘一次Swagger暴露面。上线前用curl确认/v3/api-docs等端点是否只在内网可达,对外关闭或者鉴权到位。这个过程我一般塞进发布检查清单里,跟数据库备份检查并列,养成习惯就不容易漏。
我个人的体会是,Swagger最值钱的地方不是那个绿油油的文档页面,而是它逼着团队把接口信息结构化、把描述写规范。工具本身五分钟就能跑通,但能不能让这个工具真正为团队提效,取决于平时有没有认真维护注解和描述。希望这篇文章能帮你少走点弯路,有问题也欢迎在评论区一起讨论。