一文搞懂Swagger:Spring Boot集成、注解规范与安全防护
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.0Spring 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-strategyant_path_matcher这确实是springfox的临时解药但属于治标不治本。与其加配置硬撑不如直接迁到springdoc注解迁移成本其实很低。2.2 完整接入步骤依赖、配置类、启动验证以最常见的Spring Boot 3.x Maven项目为例pom.xml加一个依赖就够dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.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/VOimport 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 zhangsanexample.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-docsspringdoc/swagger-ui/index.html、/swagger-ui/**、/v3/api-docsKnife4j/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 iddev/id activation activeByDefaulttrue/activeByDefault /activation dependencies dependency groupIdorg.springdoc/groupId artifactIdspringdoc-openapi-starter-webmvc-ui/artifactId version2.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, descriptionPython 版用户服务接口文档, version1.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如果是老项目用的springfoxv2规范改成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-strategyant_path_matcher也能顶一阵但别拖太久。坑二页面打开但接口列表为空。大概率是Controller没有被扫描到或者springdoc的packagesToScan配错了路径。对照一下Controller所在的包路径和GroupedOpenApi里的packagesToScan是否一致。另外检查Controller是否真的被Spring容器管理没有RestController注解是不会被扫描的。坑三返回的JSON里中文乱码。多数情况下是接口返回字符串时未指定produces编码。在Controller或RequestMapping上加上produces application/json;charsetUTF-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最值钱的地方不是那个绿油油的文档页面而是它逼着团队把接口信息结构化、把描述写规范。工具本身五分钟就能跑通但能不能让这个工具真正为团队提效取决于平时有没有认真维护注解和描述。希望这篇文章能帮你少走点弯路有问题也欢迎在评论区一起讨论。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →