1. 模块概述
ruoyi-gateway 是整个微服务系统的 唯一入口,基于 Spring Cloud Gateway 构建。所有客户端请求首先到达网关,经过过滤器链处理后路由到对应的后端服务。
模块职责
| 职责 | 实现组件 | 源码 |
|---|---|---|
| JWT 鉴权 | AuthFilter | 全局过滤器 order=-200 |
| 验证码校验 | ValidateCodeFilter | 全局过滤器 order=-100 |
| XSS 防护 | XssFilter | 全局过滤器 |
| 黑名单拦截 | BlackListUrlFilter | 全局过滤器 |
| Sentinel 熔断 | SentinelFallbackHandler | 降级处理器 |
| 全局异常处理 | GatewayExceptionHandler | WebExceptionHandler |
| 验证码生成 | ValidateCodeHandler | RouterFunction 端点 |
过滤器执行顺序
graph LR
REQ[客户端请求] --> XF[XssFilter
XSS过滤]
XF --> BF[BlackListUrlFilter
黑名单拦截]
BF --> VF[ValidateCodeFilter
验证码校验]
VF --> AF[AuthFilter
JWT鉴权
order=-200]
AF --> RT[路由到后端服务]
过滤器的执行顺序由 getOrder() 返回值决定,值越小优先级越高:
ValidateCodeFilter:order = -100(在鉴权前执行)AuthFilter:order = -200(在验证码后执行)XssFilter:order = -300(最先执行,最早拦截 XSS 攻击)
2. 命名体系与易混淆函数对比
2.1 命名规律拆解
| 前缀 | 模块/对象 | 操作 | 含义 |
|---|---|---|---|
Auth |
Filter |
filter |
全局鉴权过滤器,验证 JWT 并注入用户信息到请求头 |
Validate |
CodeFilter |
filter |
验证码校验过滤器,登录/注册前校验验证码 |
Xss |
Filter |
filter |
XSS 跨站脚本攻击过滤器,清理请求参数 |
BlackList |
UrlFilter |
filter |
URL 黑名单过滤器,拦截禁止访问的路径 |
Validate |
CodeHandler |
handle |
验证码生成处理器,返回验证码图片 |
Validate |
CodeService |
createCaptcha |
验证码生成服务接口 |
Validate |
CodeServiceImpl |
createCaptcha |
验证码生成服务实现,生成 base64 图片和 UUID |
命名规律总结:网关层的命名模式为 功能描述 + Filter/Handler/Service,Filter 负责拦截校验,Handler 负责响应生成,Service 负责业务逻辑。
2.2 易混淆函数对比表
ValidateCodeFilter vs ValidateCodeHandler vs ValidateCodeService
| 对比维度 | ValidateCodeFilter | ValidateCodeHandler | ValidateCodeService |
|---|---|---|---|
| 角色 | 全局过滤器(GatewayFilter) | 路由处理器(HandlerFunction) | 业务服务 |
| 触发时机 | 每个请求到达时 | GET /code 请求 | Handler 调用 |
| 操作 | 从请求中取 code_key + code,调 Redis 校验 | 生成验证码图片,将 code 存入 Redis | 生成验证码字符串和图片 |
| 一句话区分 | Filter 是”验码员”,Handler 是”发码员”,Service 是”制码厂” |
AuthFilter.filter() vs AuthFilter.addHeader() vs AuthFilter.removeHeader()
| 对比维度 | filter() | addHeader() | removeHeader() |
|---|---|---|---|
| 角色 | 主流程方法 | 辅助方法 | 辅助方法 |
| 操作 | 完整的鉴权流程 | 向请求添加 Header(带 URL 编码) | 从请求中移除 Header |
| 调用关系 | 网关框架调用 | filter() 内部调用 | filter() 内部调用 |
| 一句话区分 | filter 是总指挥,addHeader 注入用户信息,removeHeader 清除内部标识 |
3. API Signatures
调用关系图
graph TD
GW[Gateway请求入口] --> XF[XssFilter.filter]
GW --> BF[BlackListUrlFilter.filter]
GW --> VF[ValidateCodeFilter.filter]
GW --> AF[AuthFilter.filter]
AF --> GT[getToken
从Header提取JWT]
AF --> JP[JwtUtils.parseToken
解析JWT]
AF --> RH[redisService.hasKey
检查Redis]
AF --> AH[addHeader
注入用户信息]
AF --> RH2[removeHeader
清除内部标识]
VF --> VCS[ValidateCodeService
Redis校验]
VCH[ValidateCodeHandler] --> VCS2[ValidateCodeServiceImpl.createCaptcha]
VCS2 --> KTC[KaptchaTextCreator
生成验证码文本]
VCS2 --> REDIS[Redis存储验证码
有效期2分钟]
SFH[SentinelFallbackHandler] --> ER[返回限流错误]
GEH[GatewayExceptionHandler] --> ER2[返回异常信息]
完整签名
1 | // === AuthFilter === |
4. 数据结构深度解析
4.a IgnoreWhiteProperties — 为什么需要这个配置
网关需要对每个请求进行鉴权,但登录、注册、验证码获取等接口显然不需要鉴权(此时用户还未登录)。如果硬编码白名单 URL,每次添加公开接口都需要修改代码。IgnoreWhiteProperties 将白名单配置化,支持从 Nacos 配置中心动态读取,实现了 零代码维护白名单。
4.b 结构定义
1 |
|
4.c 字段三层分析表
| 字段 | 设计动机(为什么需要) | 反事实(如果去掉会怎样) | 替代方案(还能怎么做) |
|---|---|---|---|
whites |
将鉴权白名单从硬编码改为配置化,支持动态修改 | 每加一个公开接口都要改代码、重新部署 | 可以用注解标记公开接口,但网关层无法感知业务注解 |
4.d 网关请求处理状态图
stateDiagram-v2
[*] --> XSS过滤: 请求到达
XSS过滤 --> 黑名单检查: 通过
XSS过滤 --> 请求拒绝: XSS攻击检测
黑名单检查 --> 验证码校验: 非黑名单
黑名单检查 --> 请求拒绝: URL在黑名单
验证码校验 --> JWT鉴权: 通过/跳过
验证码校验 --> 请求拒绝: 验证码错误
JWT鉴权 --> 白名单检查: 取到Token
白名单检查 --> 路由转发: 在白名单中
白名单检查 --> Token解析: 需鉴权
Token解析 --> Redis校验: 解析成功
Token解析 --> 请求拒绝: Token无效
Redis校验 --> 注入Header: 登录状态有效
Redis校验 --> 请求拒绝: 登录已过期
注入Header --> 路由转发: 转发到后端服务
路由转发 --> [*]: 响应返回
请求拒绝 --> [*]: 返回错误
5. 函数逐行精讲
5.a 网关鉴权主流程
- 调用时机:每个非白名单请求到达网关时
- 典型调用者:Spring Cloud Gateway 过滤器链
- 前置条件:XSS 过滤和验证码校验已通过
- 目的:验证 JWT 有效性,将用户信息注入请求头传递给下游服务
1 |
|
5.b Token 提取
- 调用时机:filter() 方法鉴权流程中
- 典型调用者:AuthFilter.filter()
- 前置条件:请求已通过白名单检查
- 目的:从 HTTP Header 中提取纯 JWT 字符串
1 | private String getToken(ServerHttpRequest request) |
5.c 验证码校验
函数:ValidateCodeFilter.filter()
- 调用时机:每个请求到达网关时(在 AuthFilter 之前)
- 典型调用者:Spring Cloud Gateway 过滤器链
- 前置条件:请求已通过 XSS 过滤和黑名单检查
- 目的:对登录/注册请求校验验证码,防止暴力破解
1 |
|
6. 关键算法剖析
6.1 网关过滤器链的执行模型
Spring Cloud Gateway 基于 WebFlux 响应式编程,过滤器链通过 Mono<Void> 串联:
1 | 请求 → filter1 → filter2 → ... → filterN → 后端服务 |
每个 filter() 方法返回 Mono<Void>:
- 返回
chain.filter(exchange)表示继续下一个过滤器 - 返回
unauthorizedResponse(...)表示中断链路,直接返回错误响应
6.2 白名单匹配
StringUtils.matches() 使用 Spring 的 PathMatcher 进行 Ant 风格匹配:
1 | 白名单配置: /auth/**, /code/** |
7. 设计决策分析
为什么验证码校验在网关层而不是 Auth 服务中?
- 安全纵深:在网关层拦截无效请求,避免恶意流量穿透到业务服务
- 减少无效调用:如果验证码错误,直接返回 400,不需要走完整的 Feign 调用链
- Redis 就近访问:网关也配置了 Redis,可以直接校验验证码,无需远程调用
为什么 from-source 请求头要在网关层删除?
AuthFilter.removeHeader() 在第 82 行被调用:
1 | removeHeader(mutate, SecurityConstants.FROM_SOURCE); |
这是安全防御措施:from-source: inner 是服务间内部调用的标识。如果外部客户端在请求中伪造了这个 Header,InnerAuthAspect 会误认为是内部调用而放行。网关在入口处统一删除此 Header,确保只有真正来自内部 Feign 调用的请求才携带此标识。
为什么 Gateway 选择 WebFlux 响应式而非 Servlet?
Spring Cloud Gateway 基于 Spring WebFlux(响应式),与 Zuul 1.x(Servlet 阻塞式)的关键区别:
| 维度 | Gateway (WebFlux) | Zuul 1.x (Servlet) |
|---|---|---|
| 线程模型 | 事件驱动,少量线程处理大量连接 | 每请求一线程 |
| 吞吐量 | 高(非阻塞 I/O) | 受线程池大小限制 |
| 内存占用 | 低 | 较高 |
| 编程模型 | 函数式/响应式(Mono/Flux) | 命令式 |
8. 学习检查点
📝 本章小结
- 网关是整个系统的唯一入口,通过 4 个 GlobalFilter 实现 XSS 防护、黑名单、验证码和 JWT 鉴权
- AuthFilter 执行四道检查:Token 非空 → JWT 解析成功 → Redis 中存在 → 用户信息完整
- 网关在入口处删除
from-sourceHeader,防止外部客户端伪造内部调用标识 - 验证码校验放在网关层面,实现了安全纵深防御,避免恶意流量穿透到业务层
- 白名单通过
StringUtils.matches()支持 Ant 风格通配符,配置在 Nacos 中可动态修改
🤔 思考题
如果 AuthFilter 的 order 设置为比 ValidateCodeFilter 更小的值(更高优先级),会发生什么?
参考答案
如果 AuthFilter 先于 ValidateCodeFilter 执行,登录请求会被 AuthFilter 拦截——因为此时用户还没有 Token。登录接口虽然可以加入白名单跳过鉴权,但这意味着未经验证码校验的请求就能到达 Auth 服务,失去了安全纵深防御的优势。当前的设计(ValidateCodeFilter order=-100, AuthFilter order=-200)确保验证码先校验,减少无效请求对 Redis 和下游服务的冲击。
网关 AuthFilter 中
addHeader()方法为什么对值做 URL 编码?不编码会有什么问题?参考答案
URL 编码是为了防止 Header 值中的特殊字符(如中文用户名、空格、换行符)破坏 HTTP 协议格式。HTTP Header 的值在规范中只允许 ASCII 可见字符。如果不编码,包含中文的 username 可能导致 Header 解析失败或被中间代理截断。参见 ServletUtils.urlEncode() 的实现。
网关层已经做了 JWT 鉴权,为什么下游服务的 HeaderInterceptor 还要再次从 Redis 加载 LoginUser?
参考答案
网关只传递了 user_key、user_id、username 三个基础字段,而业务逻辑需要完整的角色和权限集合来做鉴权。HeaderInterceptor 从 Header 中拿到 user_key 后,通过 TokenService 从 Redis 加载完整的 LoginUser(含 permissions、roles),存入 ThreadLocal 供后续使用。此外,HeaderInterceptor 还会调用 verifyLoginUserExpire() 自动续期——网关不做续期是为了保持网关层的轻量和快速。