本文引用的文件
本文聚焦 ai-gateway 中三个网关过滤器与相关支撑组件,解释以下问题:
本说明面向既需要理解代码实现,又希望把握整体设计意图的读者。
Gateway 层围绕 Spring Cloud Gateway 的全局过滤器机制构建。本次文档涉及的核心 Java 类位于 com.zsjz.gateway.route 包和 com.zsjz.gateway.support 包,数据模型定义在 SQL 脚本中。
graph TB
Client["客户端"] --> GW["Spring Cloud Gateway"]
GW --> LRF["LoginRequestFilter<br/>提取 username"]
GW --> SRF["StickyRouteFilter<br/>按 userId/username 选节点"]
GW --> LRSF["LoginResponseFilter<br/>登录后写绑定与 token 缓存"]
SRF --> TR["TokenResolver<br/>x-token → userId"]
SRF --> URS["UserRouteService<br/>Redis + PostgreSQL 路由缓存"]
URS --> UR["UserRouteRepository<br/>gateway_user_route"]
SRF --> NS["NodeSelector<br/>健康节点轮询"]
GW --> ERR["GatewayErrorHandler<br/>统一错误码"]
GW --> AI["ai-server 节点"]
TR --> Redis["Redis"]
URS --> Redis
图表来源
章节来源
| 组件 | 主要职责 | 关键行为 |
|---|---|---|
| LoginRequestFilter | 识别并预处理登录请求 | 解析请求体中的 username;改写 Accept-Encoding;转发原请求体 |
| StickyRouteFilter | 粘性路由决策与目标 URL 重写 | 登录请求用 username 查绑定或轮询候选;普通请求用 x-token 查 userId;校验节点可用性并设置 GATEWAY_REQUEST_URL_ATTR |
| LoginResponseFilter | 登录响应后处理 | 解析成功登录响应;调用 UserRouteService 绑定用户到节点;回填 token 到 Redis |
| TokenResolver | x-token 到 userId 的解析与缓存 | 优先读 gateway:token 键;未命中则调用 sa-token;登录成功后由响应过滤器回填 |
| UserRouteService | 用户路由两级缓存 | Redis 热路径缓存;PostgreSQL gateway_user_route 为事实来源;提供 bindIfAbsent |
| UserRouteRepository | 用户路由数据库访问 | 查询绑定;幂等插入;并发冲突由部分唯一索引裁决 |
| NodeSelector | 匿名或新用户健康节点轮询 | 仅对 enabled 且 isUp 的节点轮询;已绑定用户不改变其节点 |
| StickyUnavailableException | 粘性不可用异常 | 封装 HTTP 503 语义 |
| GatewayErrorHandler | 统一错误处理器 | 将 StickyUnavailableException 转为 503 JSON,连接失败转 502,其余转 500 |
章节来源
Gateway 不是业务逻辑中心,而是认证上下文、路由粘性与后端可达性的协调者。真实登录校验、token 签发由 ai-server 完成;Gateway 只负责“把请求送到正确的节点”,并在登录成功后维护“用户→节点”的映射。
sequenceDiagram
participant C as "客户端"
participant G as "Gateway"
participant A as "ai-server 节点"
participant DB as "PostgreSQL"
participant R as "Redis"
Note over C,A: 首次登录
C->>G: POST /服务前缀/登录路径<br/>请求体含 username
G->>G: LoginRequestFilter 解析 username
G->>G: StickyRouteFilter 决定节点
alt 已有绑定
G->>DB: 查询 user_id/username 对应 node_id
else 新用户
G->>G: NodeSelector 轮询健康节点
end
G->>A: 转发登录请求
A-->>G: 登录成功 Result{code=200,data:{token,userId,username}}
G->>G: LoginResponseFilter 解析响应
G->>DB: bindIfAbsent(serviceId,userId,username,nodeId)
G->>R: cacheToken(serviceId,token,userId)
G-->>C: 原样透传登录响应
图表来源
LoginRequestFilter 是一个优先级较高的全局过滤器。它只对“已注册服务的完整登录路径”生效,并且只处理 HTTP POST 请求。
处理要点:
如果请求体为空、超过 64KB、非 JSON 或没有 username,它会记录日志并按匿名请求继续处理,由后端自行判断登录结果。
章节来源
StickyRouteFilter 是粘性路由的核心,优先级为 10001,在 Spring Cloud Gateway 内部 RouteToRequestUrlFilter(10000)之后执行。它会根据“是否是登录请求”走两条分支。
无论登录还是普通请求,StickyRouteFilter 都会:
在响应头添加 X-Gateway-Node 便于排障。
flowchart TD
Start(["进入 StickyRouteFilter"]) --> HasRoute{"是否命中路由"}
HasRoute -->|否| Next["跳过粘性逻辑"]
HasRoute -->|是| IsLogin{"是否登录请求"}
IsLogin -->|是| GetUser["取 LOGIN_USERNAME"]
GetUser --> BoundCheck{"是否有绑定节点"}
BoundCheck -->|有| UseBound["使用绑定节点"]
BoundCheck -->|无| PickCandidate["NodeSelector 轮询健康节点"]
IsLogin -->|否| GetToken["读取 x-token"]
GetToken --> TokenValid{"token 可解析?"}
TokenValid -->|否| Anonymous["匿名轮询"]
TokenValid -->|是| ResolveUserId["TokenResolver 解析 userId"]
ResolveUserId --> LookupBind{"是否绑定节点"}
LookupBind -->|是| UseBound
LookupBind -->|否| DefaultNode{"是否配置默认节点"}
DefaultNode -->|是| BindDefault["bindIfAbsent 到默认节点"]
DefaultNode -->|否| Error503["抛出粘性不可用异常"]
UseBound --> ValidateNode["校验节点启用与健康状态"]
PickCandidate --> ValidateNode
Anonymous --> ValidateNode
BindDefault --> ValidateNode
ValidateNode --> Rewrite["重写 GATEWAY_REQUEST_URL_ATTR"]
Rewrite --> End(["交给下游过滤器转发"])
Error503 --> End
图表来源
章节来源
LoginResponseFilter 的优先级为 NettyWriteResponseFilter - 1,保证在响应写出之前捕获响应体。
处理流程:
这里的关键是 chosenNodeId 来自 StickyRouteFilter 在请求阶段设置的 LOGIN_CHOSEN_NODE_ID。因此,新用户第一次登录会绑定到当时轮询到的健康节点;并发首次登录时,数据库部分唯一索引保证只有一个绑定生效。
章节来源
数据库表结构定义了 Gateway 的用户粘性路由基础:
Redis 提供两级缓存:
gateway:token:{serviceId}:{token} → userId,TTL 24 小时。
erDiagram
GATEWAY_SERVICE {
varchar id PK
varchar name
varchar path_prefix UK
varchar login_path
varchar health_path
varchar default_node_id
smallint enabled
timestamp create_time
timestamp update_time
}
GATEWAY_NODE {
varchar id PK
varchar service_id FK
varchar name
varchar base_url
smallint enabled
varchar remark
timestamp create_time
timestamp update_time
}
GATEWAY_USER_ROUTE {
bigint id PK
varchar service_id FK
bigint user_id
varchar username
varchar node_id FK
timestamp create_time
timestamp update_time
}
GATEWAY_SERVICE ||--o{ GATEWAY_NODE : "包含"
GATEWAY_SERVICE ||--o{ GATEWAY_USER_ROUTE : "拥有绑定"
GATEWAY_NODE ||--o{ GATEWAY_USER_ROUTE : "被绑定"
图表来源
章节来源
classDiagram
class LoginRequestFilter {
+filter(exchange, chain) Mono<Void>
+parseUsername(body) String
-resolveService(exchange) ServiceInfo
}
class StickyRouteFilter {
+filter(exchange, chain) Mono<Void>
-decideForLogin(exchange, service) Mono<String>
-decideForNormal(exchange, service) Mono<String>
-route(exchange, chain, service, nodeId) Mono<Void>
+buildTargetUri(baseUrl, requestUri) URI
}
class LoginResponseFilter {
+filter(exchange, chain) Mono<Void>
-parseAndBind(serviceId, chosenNodeId, body) Mono<Void>
+parseLogin(body) LoginSuccess
}
class TokenResolver {
+resolveUserId(serviceId, token) Mono<Long>
+cacheToken(serviceId, token, userId) Mono<Boolean>
-resolveFromSaToken(token) Mono<Long>
}
class UserRouteService {
+findNodeIdByUserId(serviceId, userId) Mono<String>
+findNodeIdByUsername(serviceId, username) Mono<String>
+bindIfAbsent(serviceId, userId, username, nodeId) Mono<String>
+evictUserCache(serviceId, userId, username) Mono<Void>
}
class UserRouteRepository {
+findNodeIdByUserId(...)
+findNodeIdByUsername(...)
+bindIfAbsent(...)
+deleteRoutesForUser(...)
}
class NodeSelector {
+select(service) NodeInfo
}
class StickyUnavailableException {
+httpStatus int
+serviceUnavailable(message) StickyUnavailableException
}
class GatewayErrorHandler {
+handle(exchange, error) Mono<Void>
}
LoginRequestFilter --> StickyRouteFilter : "传递 LOGIN_USERNAME"
StickyRouteFilter --> TokenResolver : "x-token → userId"
StickyRouteFilter --> UserRouteService : "查询/绑定节点"
StickyRouteFilter --> NodeSelector : "健康节点轮询"
LoginResponseFilter --> UserRouteService : "bindIfAbsent"
LoginResponseFilter --> TokenResolver : "cacheToken"
UserRouteService --> UserRouteRepository : "JDBC 访问"
StickyRouteFilter --> StickyUnavailableException : "不可用时抛出"
StickyUnavailableException --> GatewayErrorHandler : "统一转 503"
图表来源
章节来源
章节来源
现象:
原因:
排查步骤:
章节来源
现象:
原因:
排查步骤:
章节来源
现象:
原因:
排查步骤:
章节来源
现象:
原因:
排查步骤:
章节来源
现象:
原因:
排查步骤:
章节来源
Gateway 的粘性路由设计围绕三个关键点展开:
这套机制在性能、一致性和可排障性之间做了平衡:Redis 减少数据库压力,PostgreSQL 提供最终一致性,StickyRouteFilter 保证请求始终落到有用户数据的节点,GatewayErrorHandler 提供清晰的错误语义。