Spring Cloud Gateway 聚合 Swagger 接入记录

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 # 各服务自身不需要展示 UI,只暴露文档数据

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 # 网关自身不需要 api-docs

对应的网关路由必须能把 /xxx-service/** 代理到后端服务,注意 StripPrefix 去掉前缀:

spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/order-service/**
filters:
- StripPrefix=1 # 转发给后端时去掉 /order-service 前缀
- 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() {
// 从路由定义中读取服务 ID,动态注册到 Swagger UI 下拉列表
routeDefinitionLocator.getRouteDefinitions()
// 过滤掉注册中心自动生成的 ReactiveCompositeDiscoveryClient_xxx 路由
.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; // 优先级要高于其他 Filter
}
}

五、生产环境关闭 Swagger

Swagger 文档本身就是接口信息的完整暴露,生产环境必须关闭,不能依赖”反正外网访问不了”来保证安全。

通过 profile 控制:

# 各服务的 application-prod.yml
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false

网关侧同样需要关闭:

# gateway 的 application-prod.yml
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false

如果担心 profile 没生效导致文档意外暴露,可以加一层代码防护:

@Configuration
@ConditionalOnProperty(name = "springdoc.api-docs.enabled", havingValue = "true")
public class OpenApiConfig {
// 只有明确开启时才生效
}

六、常见问题

某个服务的接口在聚合页面显示不出来

按顺序排查:

  1. 直接访问 http://网关地址/该服务前缀/v3/api-docs,确认能返回 JSON
  2. 确认该服务的 springdoc.api-docs.enabled=true
  3. 确认 yaml 里 urls.url 的路径与网关路由前缀一致
  4. 检查网关路由 StripPrefix 值是否正确(前缀有几段就填几)

点击”执行”报 CORS 错误

网关 CORS 配置需要覆盖 Swagger 路径:

spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOriginPatterns: "*"
allowedMethods: "*"
allowedHeaders: "*"

springdoc 版本与 Spring Boot 的对应关系

Spring Boot 版本springdoc-openapi 版本微服务依赖网关依赖
2.x1.7.xspringdoc-openapi-uispringdoc-openapi-webflux-ui
3.x2.xspringdoc-openapi-starter-webmvc-apispringdoc-openapi-starter-webflux-ui

Spring Boot 3 升级了 javax → jakarta,springdoc 2.x 已对应适配,选 2.x 的 starter 即可,无需额外处理。