Spring Cloud Gateway 聚合 Swagger 接入记录
微服务拆了七八个服务之后,每次前端联调就开始抱怨:每个服务跑在不同端口,要开一排浏览器 Tab,搞不清哪个端口对应哪个模块。其实后端自己调试接口时也一样烦。
网关聚合 Swagger 解决的就是这个问题:所有服务的接口文档统一在网关入口访问,下拉切换服务,不用换页面也不用记端口。
一、为什么需要网关聚合
没有聚合时的状态:
- 订单服务 Swagger:
http://localhost:8081/swagger-ui/index.html - 用户服务 Swagger:
http://localhost:8082/swagger-ui/index.html - 商品服务 Swagger:
http://localhost:8083/swagger-ui/index.html
测试环境 IP 加端口号记不住,每次联调都要找人要地址。
聚合后的效果:统一访问 http://gateway-host/swagger-ui/index.html,顶部下拉选择服务名,接口文档即时切换。
二、技术选型
- springfox:老方案,3.x 后停止维护,与 Spring Boot 2.6+ 有兼容性问题,不推荐
- springdoc-openapi:活跃维护,原生支持 OpenAPI 3,Spring Boot 3 同样支持;网关聚合只需配置
swagger-ui.urls,不依赖第三方扩展
本文基于:Spring Cloud Gateway(WebFlux)+ springdoc-openapi 2.3.0
三、接入步骤
各微服务配置
每个需要暴露文档的服务(Spring MVC)引入 API 数据依赖(不含 UI):
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webmvc-api</artifactId> <version>2.3.0</version> </dependency>
|
Spring Boot 2.x 对应 springdoc-openapi-ui(版本用 1.7.x);Spring Boot 3.x 用上面的 starter 2.x 版。
各服务的 application.yml:
springdoc: api-docs: path: /v3/api-docs enabled: true swagger-ui: enabled: false
|
Controller 层用 @Tag 注解分组,@Operation 描述接口:
@Tag(name = "订单接口") @RestController @RequestMapping("/order") public class OrderController {
@Operation(summary = "创建订单") @PostMapping public Result<Long> create(@RequestBody OrderCreateDTO dto) { }
@Operation(summary = "查询订单详情") @GetMapping("/{id}") public Result<OrderVO> get(@PathVariable Long id) { } }
|
如需对文档做全局配置(标题、版本、联系信息),在任意服务加一个 OpenAPI Bean:
@Configuration public class OpenApiConfig {
@Bean public OpenAPI openAPI() { return new OpenAPI() .info(new Info() .title("订单服务 API") .version("1.0.0") .description("订单相关接口文档")); } }
|
网关聚合配置
网关服务(WebFlux)引入带 UI 的 starter:
<dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-starter-webflux-ui</artifactId> <version>2.3.0</version> </dependency>
|
手动配置模式(直接指定每个服务的文档 URL,最简单可靠):
springdoc: swagger-ui: urls: - name: 订单服务 url: /order-service/v3/api-docs - name: 用户服务 url: /user-service/v3/api-docs - name: 商品服务 url: /product-service/v3/api-docs urls-primary-name: 订单服务 api-docs: enabled: false
|
对应的网关路由必须能把 /xxx-service/** 代理到后端服务,注意 StripPrefix 去掉前缀:
spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path=/order-service/** filters: - StripPrefix=1 - id: user-service uri: lb://user-service predicates: - Path=/user-service/** filters: - StripPrefix=1
|
访问 http://gateway-host/swagger-ui/index.html 即可看到聚合后的文档。
自动发现模式(从注册中心动态读取服务列表,不用手动维护 urls):
@Configuration @Lazy(false) public class SwaggerGatewayConfig {
@Autowired private SwaggerUiConfigParameters swaggerUiConfigParameters;
@Autowired private RouteDefinitionLocator routeDefinitionLocator;
@PostConstruct public void initGroups() { routeDefinitionLocator.getRouteDefinitions() .filter(route -> !route.getId().startsWith("ReactiveCompositeDiscoveryClient")) .subscribe(route -> swaggerUiConfigParameters.addGroup(route.getId())); } }
|
自动发现模式无需维护 urls 列表,新增服务后自动出现在下拉列表,前提是路由 ID 与服务名一致。
四、认证透传问题
实际项目里几乎必踩的坑:网关有鉴权 Filter,Swagger 的文档数据请求(/v3/api-docs)被拦截,页面打开是空的或返回 401。
解决方式:在网关鉴权白名单里放开相关路径。
以 Spring Security(WebFlux)为例:
@Bean public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) { http.authorizeExchange(exchanges -> exchanges .pathMatchers( "/swagger-ui/**", "/swagger-ui.html", "/swagger-ui/index.html", "/v3/api-docs/**", "/**/v3/api-docs" ).permitAll() .anyExchange().authenticated() ); return http.build(); }
|
以自定义 GlobalFilter 为例:
@Component public class AuthFilter implements GlobalFilter, Ordered {
private static final List<String> WHITE_LIST = List.of( "/swagger-ui", "/v3/api-docs", "/swagger-ui.html" );
@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path = exchange.getRequest().getURI().getPath(); boolean isWhite = WHITE_LIST.stream().anyMatch(path::contains); if (isWhite) { return chain.filter(exchange); } }
@Override public int getOrder() { return -100; } }
|
五、生产环境关闭 Swagger
Swagger 文档本身就是接口信息的完整暴露,生产环境必须关闭,不能依赖”反正外网访问不了”来保证安全。
通过 profile 控制:
springdoc: api-docs: enabled: false swagger-ui: enabled: false
|
网关侧同样需要关闭:
springdoc: api-docs: enabled: false swagger-ui: enabled: false
|
如果担心 profile 没生效导致文档意外暴露,可以加一层代码防护:
@Configuration @ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true") public class OpenApiConfig { }
|
六、常见问题
某个服务的接口在聚合页面显示不出来
按顺序排查:
- 直接访问
http://网关地址/该服务前缀/v3/api-docs,确认能返回 JSON - 确认该服务的
springdoc.api-docs.enabled=true - 确认 yaml 里
urls.url 的路径与网关路由前缀一致 - 检查网关路由
StripPrefix 值是否正确(前缀有几段就填几)
点击”执行”报 CORS 错误
网关 CORS 配置需要覆盖 Swagger 路径:
spring: cloud: gateway: globalcors: cors-configurations: '[/**]': allowedOriginPatterns: "*" allowedMethods: "*" allowedHeaders: "*"
|
springdoc 版本与 Spring Boot 的对应关系
| Spring Boot 版本 | springdoc-openapi 版本 | 微服务依赖 | 网关依赖 |
|---|
| 2.x | 1.7.x | springdoc-openapi-ui | springdoc-openapi-webflux-ui |
| 3.x | 2.x | springdoc-openapi-starter-webmvc-api | springdoc-openapi-starter-webflux-ui |
Spring Boot 3 升级了 javax → jakarta,springdoc 2.x 已对应适配,选 2.x 的 starter 即可,无需额外处理。