Spring Cloud 学习与实践(14):集成 Knife4j,实现 JWT 调试与 Gateway 文档聚合
文章目录
Spring Cloud 学习与实践(14):集成 Knife4j,实现 JWT 调试与 Gateway 文档聚合
本章严格按照实际演练顺序推进:先让单个服务生成接口文档,再补充 OpenAPI 3 注解,随后扩展到多个微服务,最后通过 Gateway 聚合文档并完成 JWT 调试。
本章不会一开始就给出最终配置,而是保留实际过程中遇到的请求地址错误、跨域、Authorize 不生效、旧版诊断地址混用和网关前缀重复等问题。
1、本章要解决什么问题
经过前面章节的演进,当前项目已经包含:
cloud-gateway
cloud-auth
cloud-user
cloud-product
cloud-order
接口数量越来越多,仅靠代码或手工记录会出现几个明显问题:
接口分散在不同服务
↓
开发人员需要记住多个端口和路径
↓
请求参数、响应结构和状态含义不够直观
↓
受保护接口还要手工拼接 JWT 请求头
↓
联调成本逐渐增加
本章希望实现:
Controller
↓
springdoc-openapi 解析
↓
生成 OpenAPI 3 文档
↓
Knife4j 展示与调试
↓
Gateway 聚合多个服务
↓
统一使用 JWT 调试业务接口
最终开发入口为:
http://localhost:9000/doc.html
2、OpenAPI、springdoc 和 Knife4j 的关系
2.1 三者分别负责什么
| 名称 | 本质 | 当前项目中的职责 |
|---|---|---|
| OpenAPI | 接口描述规范 | 定义接口、参数、模型、安全方案等文档结构 |
| springdoc-openapi | Spring 接口解析工具 | 扫描 Controller 和注解,生成 /v3/api-docs |
| Swagger UI | OpenAPI 基础展示页面 | 提供基础文档展示和调试 |
| Knife4j | OpenAPI 增强 UI 与聚合组件 | 提供中文界面、增强调试和 Gateway 聚合 |
调用关系可以理解为:
Spring MVC Controller
↓
springdoc-openapi
↓
OpenAPI JSON
↓
Knife4j
↓
文档展示与接口调试
Knife4j 并不是替代 OpenAPI,也不是直接扫描 Controller 的核心工具。
2.2 为什么不用以前的 Knife4j 2.x
以前常见的组合是:
knife4j-spring-boot-starter 2.x
+
Springfox
+
Swagger 2
常见注解:
@Api
@ApiOperation
@ApiParam
@ApiModel
@ApiModelProperty
当前项目采用:
knife4j-openapi3-spring-boot-starter 4.5.0
+
springdoc-openapi
+
OpenAPI 3
对应注解:
@Tag
@Operation
@Parameter
@Schema
新旧注解对应关系:
| Swagger 2 / Springfox | OpenAPI 3 / springdoc |
|---|---|
@Api |
@Tag |
@ApiOperation |
@Operation |
@ApiParam |
@Parameter |
@ApiModel |
@Schema |
@ApiModelProperty |
@Schema |
Docket |
OpenAPI、GroupedOpenApi、YAML 分组 |
当前项目统一使用:
io.swagger.v3.oas.annotations.*
两套注解不要混用。
3、阶段一:只给 cloud-user 接入 Knife4j
3.1 本阶段目标
先建立最小闭环:
cloud-user 引入依赖
↓
springdoc 扫描 Controller
↓
生成 /v3/api-docs
↓
Knife4j 打开 /doc.html
此时暂不处理:
接口中文说明
JWT
Gateway
多服务聚合
这样可以先观察:不写 OpenAPI 注解时,springdoc 能自动生成多少内容。
3.2 父工程统一管理版本
父工程增加版本:
<knife4j.version>4.5.0</knife4j.version>
在 dependencyManagement 中增加:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
当前项目是 Spring Boot 2.7.18,因此使用:
knife4j-openapi3-spring-boot-starter
不要使用 Spring Boot 3 对应的 Jakarta Starter。
3.3 cloud-user 引入依赖
cloud-user/pom.xml 增加:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
</dependency>
父工程已经统一管理版本,子模块不再重复填写。
3.4 增加最小配置
在 cloud-user-dev.yaml 中增加两个顶级配置块:
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
group-configs:
- group: default
paths-to-match: /**
packages-to-scan: com.example.cloud.user.controller
knife4j:
enable: true
setting:
language: zh_cn

3.5 重启与验证
本阶段只重启:
cloud-user
依次访问:
http://localhost:9200/v3/api-docs
http://localhost:9200/swagger-ui.html
http://localhost:9200/doc.html

三者职责:
| 地址 | 作用 |
|---|---|
/v3/api-docs |
原始 OpenAPI 3 JSON |
/swagger-ui.html |
springdoc 自带页面 |
/doc.html |
Knife4j 增强页面 |
本阶段实际结果:
- 三个地址都能正常访问;
- Knife4j 能自动识别现有用户接口;
- 接口名称和字段说明仍然比较粗糙。
3.6 阶段一结论
不写 OpenAPI 注解
↓
文档仍然可以生成
但是
↓
接口名称、参数含义和模型说明不够清楚
这正是阶段二要解决的问题。
4、阶段二:补充 OpenAPI 3 注解
4.1 本阶段目标
本阶段不修改用户业务逻辑,只改善文档可读性:
设置服务标题
↓
给 Controller 分组
↓
给接口添加中文名称
↓
给参数增加说明
↓
给实体字段增加示例
4.2 新增 OpenApiConfig
创建:
cloud-user
└── com.example.cloud.user.config
└── OpenApiConfig.java
最初只设置文档信息:
@Bean
public OpenAPI userOpenApi() {
return new OpenAPI()
.info(
new Info()
.title("cloud-user 用户服务接口文档")
.description(
"提供用户查询、分页、新增、修改和删除接口"
)
.version("v1.0")
);
}
添加后,文档标题不再是:
OpenAPI definition
而是:
cloud-user 用户服务接口文档
4.3 给 User 增加 @Schema
本阶段以 User 实体作为模型注解示例:
package com.example.cloud.user.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 用户实体。
*/
@Data
@TableName("t_user")
@Schema(description = "用户信息")
public class User {
@TableId(type = IdType.AUTO)
@Schema(
description = "用户 ID",
example = "1"
)
private Long id;
@Schema(
description = "登录用户名",
example = "zhangsan"
)
private String username;
@Schema(
description = "用户昵称",
example = "张三"
)
private String nickname;
@Schema(
description = "手机号码",
example = "13800000001"
)
private String phone;
@Schema(
description = "用户状态:1-启用,0-禁用",
example = "1",
allowableValues = {"0", "1"}
)
private Integer status;
@Schema(
description = "创建时间",
example = "2026-06-01T10:00:00"
)
private LocalDateTime createTime;
}
本项目的 User 同时承担:
数据库实体
新增请求体
修改请求体
查询响应
因此没有盲目把字段全部标成必填。
这也暴露出接口契约设计中的一个问题:
同一个实体同时承担多种职责
↓
不同场景的字段约束难以准确表达
更规范的生产设计通常会拆分为:
CreateUserRequest
UpdateUserRequest
UserResponse
本章只记录该边界,不为了文档展示而突然重构已有业务代码。
4.4 给 UserController 增加注解
Controller 使用:
@Tag(
name = "用户管理",
description = "用户查询、分页、新增、修改和删除接口"
)
接口使用:
@Operation(
summary = "根据 ID 查询用户",
description = "根据用户 ID 查询单个用户"
)
参数使用:
@Parameter(
description = "用户 ID",
required = true,
example = "1"
)
请求体同时存在两个同名注解:
org.springframework.web.bind.annotation.RequestBody
负责真正读取 HTTP 请求体。
io.swagger.v3.oas.annotations.parameters.RequestBody
只负责文档说明。
为了避免 import 冲突,文档注解可以使用完整类名。
完整代码:
package com.example.cloud.user.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.cloud.common.result.ErrorCode;
import com.example.cloud.common.result.Result;
import com.example.cloud.user.entity.User;
import com.example.cloud.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 用户管理接口。
*/
@Tag(
name = "用户管理",
description = "用户查询、分页、新增、修改和删除接口"
)
@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 查询全部用户。
*/
@Operation(
summary = "查询全部用户",
description = "返回用户表中的全部用户。"
+ "数据量较大时应优先使用分页查询接口。"
)
@GetMapping
public Result<List<User>> list() {
List<User> users = userService.list();
return Result.success(users);
}
/**
* 根据 ID 查询用户。
*/
@Operation(
summary = "根据 ID 查询用户",
description = "根据用户 ID 查询单个用户;"
+ "用户不存在时返回资源不存在错误。"
)
@GetMapping("/{id}")
public Result<User> getById(
@Parameter(
description = "用户 ID",
required = true,
example = "1"
)
@PathVariable Long id
) {
User user = userService.getById(id);
if (user == null) {
return Result.fail(
ErrorCode.NOT_FOUND,
"用户不存在"
);
}
return Result.success(user);
}
/**
* 新增用户。
*/
@Operation(
summary = "新增用户",
description = "新增一条用户记录。"
+ "用户 ID 和创建时间通常由数据库生成,"
+ "调用方无需主动传入。"
)
@PostMapping
public Result<Void> create(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "新增用户信息",
required = true
)
@RequestBody User user
) {
boolean success = userService.save(user);
if (!success) {
return Result.fail(
ErrorCode.BIZ_ERROR,
"新增用户失败"
);
}
return Result.success();
}
/**
* 修改用户。
*/
@Operation(
summary = "修改用户",
description = "根据请求体中的用户 ID 修改用户信息;"
+ "用户 ID 不能为空。"
)
@PutMapping
public Result<Void> update(
@io.swagger.v3.oas.annotations.parameters.RequestBody(
description = "需要修改的用户信息,必须包含用户 ID",
required = true
)
@RequestBody User user
) {
if (user.getId() == null) {
return Result.fail(
ErrorCode.PARAM_ERROR,
"用户 ID 不能为空"
);
}
boolean success = userService.updateById(user);
if (!success) {
return Result.fail(
ErrorCode.NOT_FOUND,
"用户不存在或修改失败"
);
}
return Result.success();
}
/**
* 删除用户。
*/
@Operation(
summary = "根据 ID 删除用户",
description = "根据用户 ID 删除一条用户记录。"
)
@DeleteMapping("/{id}")
public Result<Void> delete(
@Parameter(
description = "需要删除的用户 ID",
required = true,
example = "6"
)
@PathVariable Long id
) {
boolean success = userService.removeById(id);
if (!success) {
return Result.fail(
ErrorCode.NOT_FOUND,
"用户不存在或删除失败"
);
}
return Result.success();
}
/**
* 分页查询用户。
*/
@Operation(
summary = "分页查询用户",
description = "按照当前页和每页数量分页查询用户。"
)
@GetMapping("/page")
public Result<Page<User>> page(
@Parameter(
description = "当前页,从 1 开始",
example = "1"
)
@RequestParam(defaultValue = "1") long current,
@Parameter(
description = "每页记录数",
example = "10"
)
@RequestParam(defaultValue = "10") long size
) {
Page<User> page = userService.page(
new Page<>(current, size)
);
return Result.success(page);
}
}
4.5 验证结果
重启 cloud-user 后:
/v3/api-docs
中可以看到:
用户管理
查询全部用户
根据 ID 查询用户
分页查询用户
Knife4j 页面中也能看到:
- 中文接口分组;
- 中文接口名称;
- 路径参数说明;
- 分页参数示例;
- User 字段描述;
Result<T>自动解析出的code、message、data。

4.6 为什么只完整注解用户服务
用户服务已经覆盖:
GET
POST
PUT
DELETE
路径参数
查询参数
请求体
分页
泛型响应
并演示了:
@Tag
@Operation
@Parameter
@RequestBody
@Schema
继续给认证、商品和订单接口重复添加同类注解,主要是复制工作,新的学习收益很低。
因此本章明确采用:
| 服务 | 文档策略 |
|---|---|
cloud-user |
完整 OpenAPI 注解示例 |
cloud-auth |
springdoc 自动生成基础文档 |
cloud-product |
springdoc 自动生成基础文档 |
cloud-order |
springdoc 自动生成基础文档 |
这不是遗漏,而是主动控制演练范围。
5、阶段三:扩展到其他微服务
5.1 本阶段目标
让下面三个服务也能独立生成文档:
cloud-auth
cloud-product
cloud-order
本阶段暂不做 Gateway 聚合。
5.2 三个服务增加依赖
分别在三个模块中增加:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
</dependency>
5.3 分别增加 springdoc 配置
认证服务:
springdoc:
api-docs:
path: /v3/api-docs
swagger-ui:
path: /swagger-ui.html
group-configs:
- group: default
paths-to-match: /**
packages-to-scan: com.example.cloud.auth.controller
knife4j:
enable: true
setting:
language: zh_cn
商品服务和订单服务只需要修改扫描包路径。
5.4 分别增加文档标题
认证服务:
.title("cloud-auth 认证服务接口文档")
商品服务:
.title("cloud-product 商品服务接口文档")
订单服务:
.title("cloud-order 订单服务接口文档")
以认证服务为例:
package com.example.cloud.auth.config;
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 authOpenApi() {
return new OpenAPI()
.info(
new Info()
.title("cloud-auth 认证服务接口文档")
.description("提供用户登录和认证相关接口")
.version("v1.0")
);
}
}

5.5 验证四个独立入口
| 服务 | 文档地址 |
|---|---|
| 认证 | http://localhost:9100/doc.html |
| 用户 | http://localhost:9200/doc.html |
| 商品 | http://localhost:9300/doc.html |
| 订单 | http://localhost:9400/doc.html |
四个页面均可正常打开。
此时也出现新的问题:
文档已经生成
↓
但是开发人员仍要记住四个地址
后面的 Gateway 聚合将解决这个问题。
6、阶段四:让文档调试真正经过 Gateway
6.1 为什么不能直接调用业务端口
用户文档页面默认发出:
http://localhost:9200/users/1
这会绕过 Gateway。
但当前项目真实鉴权链路是:
客户端
↓
Gateway 校验 JWT
↓
Gateway 提取 userId
↓
写入 X-User-Id
↓
下游 UserContext
所以文档调试应该走:
http://localhost:9000/api/user/users/1
6.2 调整认证服务OpenApiConfig
修改:
cloud-auth
└── src/main/java
└── com.example.cloud.auth.config
└── OpenApiConfig.java
将 Bean 调整为:
@Bean
public OpenAPI authOpenApi() {
return new OpenAPI()
.info(
new Info()
.title("cloud-auth 认证服务接口文档")
.description("提供用户登录和认证相关接口")
.version("v1.0")
)
.addServersItem(
new Server()
.url("http://localhost:9000/api")
.description("Gateway 统一入口")
);
}
6.3 调整用户服务 OpenApiConfig:
.package com.example.cloud.user.config;
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 用户服务 OpenAPI 文档配置。
*/
@Configuration
public class OpenApiConfig {
/**
* OpenAPI 安全方案名称。
*
* Components 中注册的名称,
* 必须和 SecurityRequirement 引用的名称一致。
*/
private static final String SECURITY_SCHEME_NAME = HttpHeaders.AUTHORIZATION;
@Bean
public OpenAPI userOpenApi() {
return new OpenAPI()
.info(
new Info()
.title(
"cloud-user 用户服务接口文档"
)
.description(
"提供用户查询、分页、新增、"
+ "修改和删除接口"
)
.version("v1.0")
)
/*
* Knife4j 页面虽然由 9200 端口提供,
* 但调试接口时统一请求 Gateway。
*/
.addServersItem(
new Server()
.url(
"http://localhost:9000"
+ "/api/user"
)
.description("Gateway 统一入口")
)
/*
* 声明 Bearer JWT 安全方案。
*/
.components(
new Components()
.addSecuritySchemes(
SECURITY_SCHEME_NAME,
new SecurityScheme()
.type(
SecurityScheme
.Type
.HTTP
)
.scheme("bearer")
.bearerFormat("JWT")
.description(
"登录后获得的 JWT。"
+ "授权时只粘贴 "
+ "Token 本身,"
+ "不手动添加 "
+ "Bearer 前缀。"
)
)
);
}
}
6.4 调整商品服务 OpenApiConfig:
private static final String SECURITY_SCHEME_NAME = HttpHeaders.AUTHORIZATION;
@Bean
public OpenAPI productOpenApi() {
return new OpenAPI()
.info(
new Info()
.title(
"cloud-product 商品服务接口文档"
)
.description(
"提供商品查询、管理和库存扣减接口"
)
.version("v1.0")
)
.addServersItem(
new Server()
.url(
"http://localhost:9000"
+ "/api/product"
)
.description("Gateway 统一入口")
)
.components(
new Components()
.addSecuritySchemes(
SECURITY_SCHEME_NAME,
new SecurityScheme()
.type(
SecurityScheme
.Type
.HTTP
)
.scheme("bearer")
.bearerFormat("JWT")
.description(
"登录后获得的 JWT。"
+ "只粘贴 "
+ "Token 本身。"
)
)
);
}
6.5 调整订单服务 OpenApiConfig:
private static final String SECURITY_SCHEME_NAME = HttpHeaders.AUTHORIZATION;
@Bean
public OpenAPI orderOpenApi() {
return new OpenAPI()
.info(
new Info()
.title(
"cloud-order 订单服务接口文档"
)
.description(
"提供订单创建、查询及 MQ 演练接口"
)
.version("v1.0")
)
.addServersItem(
new Server()
.url(
"http://localhost:9000"
+ "/api/order"
)
.description("Gateway 统一入口")
)
.components(
new Components()
.addSecuritySchemes(
SECURITY_SCHEME_NAME,
new SecurityScheme()
.type(
SecurityScheme
.Type
.HTTP
)
.scheme("bearer")
.bearerFormat("JWT")
.description(
"登录后获得的 JWT。"
+ "只粘贴 "
+ "Token 本身。"
)
)
);
}
6.6 先观察用户Network请求
重启:
cloud-auth
cloud-user
cloud-product
cloud-order
打开:
http://localhost:9200/doc.html
选择:
根据 ID 查询用户
先确认页面显示的请求地址已经变成:
http://localhost:9000/api/user/users/1
而不是:
http://localhost:9200/users/1
然而结果是:
OpenAPI JSON 中确实出现:
"servers": [
{
"url": "http://localhost:9000/api/user"
}
]
但实际 Network 请求仍然是:
http://localhost:9200/users/1



这说明:
OpenAPI servers 已生效
↓
但 Knife4j 当前调试请求没有自动使用它
6.7 servers 不等于实际调试 Host
最终在 Nacos 中cloud-user-dev.yaml增加:
knife4j:
enable: true
setting:
language: zh_cn
# 开启 Knife4j 调试请求的自定义 Host。
enable-host: true
# 调试用户服务接口时,统一通过 Gateway 访问。
enable-host-text: http://localhost:9000/api/user
Knife4j 自定义 Host 会参与调试请求的基础地址拼接。

修改配置后:
1. 重启 cloud-user
2. 浏览器对 /doc.html 执行 Ctrl + F5
3. 必要时关闭当前页面,重新打开

用户接口:
Host:
http://localhost:9000/api/user
接口路径:
/users/1
最终:
http://localhost:9000/api/user/users/1
另外三个服务分别配置:
认证服务:
knife4j:
enable: true
setting:
language: zh_cn
enable-host: true
enable-host-text: http://localhost:9000/api
商品服务:
knife4j:
enable: true
setting:
language: zh_cn
enable-host: true
enable-host-text: http://localhost:9000/api/product
订单服务:
knife4j:
enable: true
setting:
language: zh_cn
enable-host: true
enable-host-text: http://localhost:9000/api/order
6.8 解决跨域
独立用户文档页面来源:
http://localhost:9200
调试目标:
http://localhost:9000
端口不同,属于跨域。
Gateway 的 CORS 配置中增加:
allowedOrigins:
- "http://localhost:9100"
- "http://localhost:9200"
- "http://localhost:9300"
- "http://localhost:9400"

同时放行:
GET
POST
PUT
DELETE
OPTIONS
JWT Filter 也必须放行 OPTIONS 预检请求。
修复后,未携带 Token 请求用户接口,能够正常到达 Gateway 并返回 401。
这证明请求已经不再绕过网关。
6.9 配置 JWT 调试
通过认证服务文档登录
打开:
http://localhost:9100/doc.html
进入登录接口:
POST /auth/login
请求体使用当前测试账号,例如:
{
"username": "zhangsan",
"password": "123456"
}
页面显示的最终请求地址应为:
http://localhost:9000/api/auth/login
预期返回:
{
"code": 0,
"message": "success",
"data": {
"token": "eyJ...",
"tokenType": "Bearer",
"expiresIn": 3600,
"userId": 1,
"username": "zhangsan"
}
}
复制:
data.token
只复制三段式 JWT 本身:
eyJ...xxx.yyy
不要复制:
Bearer eyJ...
因为 OpenAPI 的 HTTP Bearer 安全方案会自动拼接:
Authorization: Bearer <token>

点击左侧:
Authorize
在 Authorization中只粘贴 Token 本身。
结果未出现预期中的请求头Authorization


实际现象:
Authorize 中填写 Token
↓
页面显示已经授权
↓
接口请求仍然没有 Authorization
↓
Gateway 返回 401
为了先确认 Token 和 Gateway 没有问题,临时使用:
文档管理
↓
全局参数设置
手工添加:
参数名称:Authorization
参数类型:header
参数值:Bearer <JWT>

请求成功。
这一步证明:
Token 正常
Gateway 正常
路由正常
问题在 Knife4j 对安全方案的应用
6.10 添加Operation.security
仅声明 SecurityScheme 还不够,OpenAPI 3 的安全配置有两层:
components.securitySchemes
↓
定义系统支持什么认证方案
Operation.security
↓
声明具体接口使用哪种认证方案
如果接口本身没有 security,Knife4j 会认为该接口不需要认证。
不希望给每个方法都加:
@SecurityRequirement
因此给cloud-user、cloud-product、cloud-order三个服务的OpenApiConfig增加:
@Bean
public GlobalOpenApiCustomizer bearerAuthGlobalOpenApiCustomizer() {
return openApi -> {
if (openApi.getPaths() == null) {
return;
}
openApi.getPaths().values().forEach(
pathItem -> pathItem
.readOperations()
.forEach(
operation ->
operation.addSecurityItem(
new SecurityRequirement()
.addList(
SECURITY_SCHEME_NAME
)
)
)
);
};
}
这个定制器为当前服务的所有 Operation 增加安全方案。
认证服务不添加该定制器,因为登录接口本来就是匿名接口。
需要区分:
| 名称 | 含义 |
|---|---|
Authorize |
Knife4j 左侧授权入口 |
Authorization |
标准 HTTP 请求头 |
Bearer |
Authorization 请求头中的认证方案 |
| JWT | Bearer 后面的 Token 内容 |
最终请求格式:
Authorization: Bearer eyJ...
重启服务刷新文档并删除之前手工添加的全局参数,最终只保留左侧 Authorize。
7、阶段五:通过 Gateway 聚合四个服务文档
7.1 本阶段目标
将四个独立入口统一为:
http://localhost:9000/doc.html
并在左上角切换:
认证服务
用户服务
商品服务
订单服务
7.2 Gateway 引入聚合组件
父工程管理:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
cloud-gateway 引入:
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-gateway-spring-boot-starter</artifactId>
</dependency>
Gateway 的职责是聚合子服务文档,不需要扫描业务 Controller。
7.3 认证服务文档路由的特殊处理
用户、商品和订单路由都使用:
StripPrefix=2
所以:
/api/user/v3/api-docs
↓
/v3/api-docs
可以直接获取文档。
认证业务路由使用:
/api/auth/**
StripPrefix=1
如果直接请求:
/api/auth/v3/api-docs
下游会收到:
/auth/v3/api-docs
而真实文档地址是:
/v3/api-docs
因此增加专用文档路由:
spring:
cloud:
gateway:
routes:
# 认证服务专用文档路由。
#
# /api/auth-docs/v3/api-docs
# 删除前两段 /api/auth-docs 后,
# cloud-auth 实际收到 /v3/api-docs。
- id: cloud-auth-docs
uri: lb://cloud-auth
predicates:
- Path=/api/auth-docs/**
filters:
- StripPrefix=2

7.4 手动聚合配置
knife4j:
gateway:
enabled: true
strategy: manual
tags-sorter: order
operations-sorter: order
routes:
- name: 认证服务
service-name: cloud-auth
url: /api/auth-docs/v3/api-docs?group=default
context-path: /
order: 1
- name: 用户服务
service-name: cloud-user
url: /api/user/v3/api-docs?group=default
context-path: /
order: 2
- name: 商品服务
service-name: cloud-product
url: /api/product/v3/api-docs?group=default
context-path: /
order: 3
- name: 订单服务
service-name: cloud-order
url: /api/order/v3/api-docs?group=default
context-path: /
order: 4

其中:
| 配置 | 作用 |
|---|---|
name |
左上角服务名称 |
url |
Gateway 获取子服务 OpenAPI JSON 的地址 |
service-name |
服务标识 |
context-path |
聚合时补充的业务前缀 |
order |
服务排序 |
四个服务都使用:
?group=default
没有冲突。
分组名称来自 name,不是 group 参数。
7.5 放行文档资源
Gateway 统一鉴权会拦截文档页面和文档 JSON。
开发环境需要放行:
/doc.html
/favicon.ico
/webjars/**
/v3/api-docs/**
/api/auth-docs/v3/api-docs/**
/api/user/v3/api-docs/**
/api/product/v3/api-docs/**
/api/order/v3/api-docs/**
最终由 JwtAuthGlobalFilter 中的文档资源判断负责放行。不要重写整个 JwtAuthGlobalFilter,只在当前白名单判断中补充文档资源。可以在现有过滤器中增加一个辅助方法:
/**
* 判断当前请求是否属于 Knife4j 或 OpenAPI 文档资源。
*
* 这些资源只在当前开发学习环境中放行。
*/
private boolean isApiDocumentResource(String path) {
return "/doc.html".equals(path)
|| "/favicon.ico".equals(path)
|| path.startsWith("/webjars/")
|| path.startsWith("/swagger-resources")
|| path.startsWith("/v3/api-docs")
|| path.startsWith(
"/api/auth-docs/v3/api-docs"
)
|| path.startsWith(
"/api/user/v3/api-docs"
)
|| path.startsWith(
"/api/product/v3/api-docs"
)
|| path.startsWith(
"/api/order/v3/api-docs"
);
}

生产环境不能直接沿用该策略。
7.6 先逐个验证文档 JSON
聚合页面之前,先访问:
http://localhost:9000/api/auth-docs/v3/api-docs?group=default
http://localhost:9000/api/user/v3/api-docs?group=default
http://localhost:9000/api/product/v3/api-docs?group=default
http://localhost:9000/api/order/v3/api-docs?group=default

四个地址都正常返回 JSON 后,再排查聚合页面。
7.7 注意新版诊断地址
排查聚合分组时,曾访问:
http://localhost:9000/swagger-resources

结果是 404。
原因是:
/swagger-resources
属于旧 Springfox / Swagger 2 路线
当前项目采用:
springdoc-openapi
+
OpenAPI 3
正确诊断地址:
http://localhost:9000/v3/api-docs/swagger-config

最终确认其中 urls 数组包含四项。
这一问题没有影响项目本身,但说明排查时不能混用两代技术栈。
7.8 左上角切换服务
打开聚合页面时,只看到认证服务。
需要点击左上角下拉框切换:
认证服务
用户服务
商品服务
订单服务

8、最终验证链路
8.1 登录
在聚合页面切换到认证服务:
POST /auth/login
实际请求:
http://localhost:9000/api/auth/login
登录成功后复制 data.token。

8.2 Authorize
在业务服务分组中点击:
Authorize
只填写 JWT 本身:
eyJ...
不要手工增加:
Bearer
Knife4j 自动生成:
Authorization: Bearer eyJ...
8.3 用户服务
GET /users/1
实际请求:
http://localhost:9000/api/user/users/1
正常返回用户信息。
8.4 商品服务
GET /products/1
实际请求:
http://localhost:9000/api/product/products/1
正常返回商品信息。
8.5 订单服务
GET /context/user-id
实际请求:
http://localhost:9000/api/order/context/user-id
正常返回 JWT 中的真实用户 ID。

9、几个容易混淆的配置
| 配置 | 真实职责 |
|---|---|
springdoc.group-configs |
控制 Controller 扫描与 OpenAPI 分组 |
OpenAPI.info |
设置标题、说明和版本 |
OpenAPI.servers |
描述接口服务器 |
knife4j.setting.enable-host-text |
控制独立文档页面的实际调试 Host |
components.securitySchemes |
定义认证方案 |
Operation.security |
声明接口使用该认证方案 |
GlobalOpenApiCustomizer |
批量给所有 Operation 增加认证 |
Gateway routes.url |
聚合时获取文档 JSON |
Gateway context-path |
聚合时补充业务前缀 |
/v3/api-docs/swagger-config |
查看 OpenAPI 3 聚合分组 |
| JWT 文档白名单 | 开发环境放行文档资源 |
10、Knife4j 与相关工具对比
10.1 Knife4j 与 Swagger UI
| 对比项 | Knife4j | Swagger UI |
|---|---|---|
| 文档规范 | OpenAPI | OpenAPI |
| 中文体验 | 较好 | 基础 |
| 接口调试 | 支持 | 支持 |
| 分组与增强功能 | 更丰富 | 相对基础 |
| Gateway 聚合 | 提供专用组件 | 通常需要自行处理 |
Knife4j 不是 OpenAPI 的替代者,而是增强展示层。
10.2 Knife4j 与 Springfox
| 对比项 | 当前方案 | 旧方案 |
|---|---|---|
| 解析框架 | springdoc-openapi | Springfox |
| 文档规范 | OpenAPI 3 | Swagger 2 |
| 原始地址 | /v3/api-docs |
/v2/api-docs |
| 聚合诊断 | /v3/api-docs/swagger-config |
/swagger-resources |
| 常用注解 | @Tag、@Operation |
@Api、@ApiOperation |
新项目更适合直接使用 OpenAPI 3 路线。
10.3 Knife4j 与 Apifox、Postman
| 工具 | 更适合的场景 |
|---|---|
| Knife4j | 接口文档随代码生成,开发环境快速联调 |
| Swagger UI | 基础 OpenAPI 展示 |
| Apifox | 接口设计、Mock、测试、团队协作 |
| Postman | 请求调试、集合管理、自动化测试 |
| YApi | 团队接口管理和 Mock |
它们不是完全互斥的。
实际团队可以:
springdoc 生成 OpenAPI
↓
Knife4j 日常查看
↓
导入 Apifox 或 Postman 做团队协作
11、生产环境边界
11.1 关闭业务服务文档
生产配置:
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false
knife4j:
enable: false
11.2 关闭 Gateway 聚合
knife4j:
gateway:
enabled: false
11.3 收紧 Gateway 白名单
生产环境不应继续匿名放行:
/doc.html
/webjars/**
/v3/api-docs/**
各服务文档 JSON 路径
仅关闭 UI 不够,OpenAPI JSON 也可能暴露接口结构。
11.4 文档安全声明不等于服务端鉴权
下面这些配置:
SecurityScheme
SecurityRequirement
GlobalOpenApiCustomizer
只负责:
告诉 Knife4j 如何携带请求头
真正校验 JWT 的仍然是:
JwtAuthGlobalFilter
即:
文档安全声明
≠
服务端安全实现
12、最终架构
cloud-auth
cloud-user
cloud-product
cloud-order
↓
各自生成 /v3/api-docs
↓
cloud-gateway 聚合
↓
http://localhost:9000/doc.html
↓
Authorize 填写 JWT
↓
Authorization: Bearer <JWT>
↓
Gateway 统一鉴权
↓
转发到业务服务
13、本章总结
本章完成了:
单服务 Knife4j
↓
OpenAPI 3 注解
↓
多服务独立文档
↓
Gateway 调试
↓
JWT Authorize
↓
Gateway 聚合
↓
生产关闭边界
最重要的不是记住几个配置,而是理解这些职责:
springdoc 负责生成文档
Knife4j 负责展示与调试
Gateway 负责聚合与鉴权
OpenAPI Security 负责描述认证
JwtAuthGlobalFilter 负责真正校验 JWT
本章也证明了一点:
文档页面看起来正确
不代表真实请求一定正确
最终仍然要通过浏览器 Network 检查:
请求地址
请求头
状态码
实际响应
14、下一章预告
第 14 章解决了微服务接口分散、说明不清、JWT 调试困难和文档入口不统一的问题。
第 15 章将引入:
TraceId
MDC
日志格式
Gateway 生成链路标识
Feign 透传
异步线程上下文传播
目标是让一次请求经过 Gateway、用户服务、商品服务和订单服务时,可以通过同一个 TraceId 串联完整日志链路。
更多推荐

所有评论(0)