Spring Cloud Gateway
本页负责 Gateway 基础、Route/Predicate/Filter、常见配置和入门排查。请求体、EventLoop、连接池、流式/WebSocket、动态路由和优雅停机请继续阅读:
其中动态路由、灰度路由、路由版本原子替换、稳定Hash灰度和404/503串路由排查,建议直接看:动态路由控制面与灰度Runbook。
Spring Cloud Gateway 是 Spring Cloud 体系里的 API 网关,常用于统一入口、路由转发、鉴权、限流、跨域、灰度发布和日志追踪。
阅读入口:一条外部请求如何穿过网关
按“Route 匹配 → Predicate 判断 → Filter 链 → lb:// 选实例 → Netty 转发 → 响应过滤”阅读。先理解入口职责,再看动态路由、OAuth2、限流和生产治理。
核心原理:Route、Predicate、Filter 与 Handler 链
Gateway 启动时把配置或服务发现结果转换成 Route;请求进入后由 Predicate 判断是否匹配,再按过滤器链修改请求、执行鉴权和治理,最终由 NettyRoutingFilter 转发。Gateway 是入口控制面,不应承载订单、支付等领域业务。
flowchart TD
A["请求进入Reactor Netty"] --> B["RoutePredicateHandlerMapping"]
B --> C["Predicate匹配Route"]
C --> D["GlobalFilter + GatewayFilter"]
D --> E["鉴权/限流/Trace/改写"]
E --> F["lb://服务名交给LoadBalancer"]
F --> G["NettyRoutingFilter转发"]
G --> H["响应过滤并返回"]零基础可以把网关理解成系统的“大门”:客户端先访问网关,网关再根据规则把请求转发到后端服务。
为什么需要网关
微服务系统里可能有很多服务:
- 用户服务。
- 文章服务。
- 评论服务。
- 文件服务。
- 搜索服务。
如果客户端直接访问每个服务,会有很多问题:
- 客户端要记住多个服务地址。
- 每个服务都要重复做鉴权。
- 跨域、限流、日志重复实现。
- 服务地址变化会影响客户端。
网关可以把这些通用能力集中处理。
请求流程
flowchart TD
A["客户端请求"] --> B["Gateway 接收请求"]
B --> C["匹配 Route"]
C --> D["执行 Predicate 判断"]
D --> E["执行前置 Filter"]
E --> F["转发到下游服务"]
F --> G["下游服务返回响应"]
G --> H["执行后置 Filter"]
H --> I["返回客户端"]Gateway 底层为什么适合做入口流量治理
Spring Cloud Gateway 基于 WebFlux 和 Reactor Netty,采用非阻塞模型处理请求。它更适合做高并发入口转发、过滤和限流。
要注意:非阻塞不代表“业务一定快”,它的核心是不要让线程长期卡在等待 IO 上。如果你在 Gateway Filter 里写阻塞数据库查询、长时间 HTTP 调用,就会把网关线程拖住,入口吞吐会明显下降。
flowchart TD
A["Gateway 少量事件循环线程"] --> B["处理大量连接"]
B --> C{"Filter 是否阻塞"}
C -- "不阻塞" --> D["线程快速处理下一个请求"]
C -- "阻塞" --> E["事件循环线程被占住"]
E --> F["入口请求排队甚至超时"]所以网关适合做“轻量横切逻辑”,不适合做复杂业务处理。
核心概念
| 概念 | 说明 |
|---|---|
| Route | 路由规则,决定请求转发到哪里 |
| Predicate | 匹配条件,例如 Path、Header、Method |
| Filter | 请求和响应的处理逻辑 |
| GlobalFilter | 对所有路由生效的全局过滤器 |
| GatewayFilter | 对某个路由生效的过滤器 |
Route、Predicate、Filter 怎么协作
一次请求进入 Gateway 后,会先找匹配的 Route。Route 匹配上以后,再执行该 Route 上绑定的 Filter 和全局 Filter。
flowchart TD
A["请求 /api/assets/100"] --> B["遍历 RouteDefinition"]
B --> C{"Path Predicate 是否匹配"}
C -- "否" --> B
C -- "是" --> D["选中 routeId=asset-service"]
D --> E["执行 GlobalFilter 前置逻辑"]
E --> F["执行 GatewayFilter 前置逻辑"]
F --> G["根据 uri=lb://asset-service 找实例"]
G --> H["转发请求"]
H --> I["下游响应"]
I --> J["执行 Filter 后置逻辑"]
J --> K["返回调用方"]Route 可以理解为“去哪儿”,Predicate 是“什么请求走这条路”,Filter 是“走之前和回来之后做什么”。
Gateway 启动阶段做了什么
Gateway 不是每次请求来了才去临时解析 YAML。应用启动时,Gateway 会把配置文件、注册中心路由、代码中声明的路由加载成 RouteDefinition,再转换成运行期可用的 Route。
flowchart TD
A["读取 application.yml"] --> B["RouteDefinitionLocator"]
C["读取注册中心服务"] --> B
D["代码 DSL 路由"] --> B
B --> E["生成 RouteDefinition"]
E --> F["转换为 Route"]
F --> G["放入 RouteLocator"]
G --> H["请求到达时按 Route 匹配"]几个对象要分清:
| 对象 | 阶段 | 作用 |
|---|---|---|
RouteDefinition | 配置阶段 | 保存配置里的 routeId、uri、predicates、filters |
Route | 运行阶段 | 真正用于请求匹配和转发 |
RouteLocator | 运行阶段 | 提供当前可用路由列表 |
RoutePredicateHandlerMapping | 请求阶段 | 判断当前请求命中哪条 Route |
FilteringWebHandler | 请求阶段 | 组装并执行过滤器链 |
所以 Gateway 的核心不是“一个 Controller 转发请求”,而是:
路由定义加载
-> 请求进入 WebFlux
-> RoutePredicateHandlerMapping 找路由
-> FilteringWebHandler 执行过滤器链
-> NettyRoutingFilter 转发请求
-> NettyWriteResponseFilter 写回响应一次请求在 Gateway 内部怎么走
下面把一次请求拆细。以 /api/assets/100 转发到 lb://asset-service 为例:
flowchart TD
A["客户端请求 /api/assets/100"] --> B["Reactor Netty 接收连接"]
B --> C["WebFlux 构造 ServerWebExchange"]
C --> D["RoutePredicateHandlerMapping 匹配 Route"]
D --> E{"是否命中路由?"}
E -->|"否"| F["返回 404"]
E -->|"是"| G["把 routeId 写入 Exchange 属性"]
G --> H["FilteringWebHandler 组装过滤器链"]
H --> I["执行前置过滤器"]
I --> J["lb://asset-service 解析实例"]
J --> K["NettyRoutingFilter 转发请求"]
K --> L["下游服务处理"]
L --> M["NettyWriteResponseFilter 写回响应"]
M --> N["执行后置过滤器"]ServerWebExchange 可以理解为 Gateway 中的一次请求上下文,里面包含:
| 内容 | 用途 |
|---|---|
| Request | 请求路径、方法、Header、Query、Body |
| Response | 响应状态码、Header、Body |
| Attributes | routeId、目标 URI、traceId 等中间状态 |
| Principal | 登录用户信息 |
很多 Gateway 原理都围绕 exchange 展开:过滤器读取请求、修改请求、写入属性、决定是否继续 chain.filter(exchange)。
Filter 链为什么有前置和后置
Gateway Filter 的代码看起来只有一个 filter 方法,但因为 Reactor 的链式回调,它天然可以做“请求前”和“响应后”两段逻辑。
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
// 这里是响应回来之后执行
}));执行顺序类似洋葱模型:
flowchart TD
A["GlobalFilter A 前置"] --> B["GlobalFilter B 前置"]
B --> C["GatewayFilter C 前置"]
C --> D["转发下游"]
D --> E["GatewayFilter C 后置"]
E --> F["GlobalFilter B 后置"]
F --> G["GlobalFilter A 后置"]如果你在前置逻辑里直接写响应并返回,不调用 chain.filter(exchange),请求就不会继续转发:
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();这就是鉴权失败、限流失败可以直接在网关返回 401/429 的原因。
Filter 的前置和后置
Gateway Filter 不是只有请求前处理,也可以在响应回来后处理。
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import java.util.concurrent.TimeUnit;
import reactor.core.publisher.Mono;
@Component
public class TraceGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String requestTraceId = exchange.getRequest().getHeaders().getFirst("X-Trace-Id");
final String traceId = requestTraceId == null
? java.util.UUID.randomUUID().toString()
: requestTraceId;
ServerWebExchange mutatedExchange = exchange.mutate()
.request(builder -> builder.header("X-Trace-Id", traceId))
.build();
long startNanos = System.nanoTime();
return chain.filter(mutatedExchange)
.doFinally(signal -> {
long cost = TimeUnit.NANOSECONDS.toMillis(
System.nanoTime() - startNanos);
System.out.println("gateway cost=" + cost + "ms, traceId=" + traceId);
});
}
@Override
public int getOrder() {
return -100;
}
}这个 Demo 做了两件事:
- 请求前:补充
X-Trace-Id。 - 响应后:记录网关耗时。
实际生产中应使用日志框架,不要用 System.out.println。
Filter 顺序为什么重要
多个 Filter 同时存在时,顺序不对会直接改变行为。
| 顺序问题 | 后果 |
|---|---|
| 日志 Filter 在 traceId 生成前执行 | 日志里没有 traceId |
| 鉴权 Filter 在白名单判断后执行错误 | 公开接口被拦截或私有接口放行 |
| StripPrefix 在 RewritePath 后顺序不对 | 下游路径错乱,出现 404 |
| 限流在鉴权前按匿名 IP 做 key | 同一出口用户互相影响 |
| 响应包装在文件下载后执行 | 文件流被破坏 |
建议顺序:
生成 traceId
-> 记录访问日志开始时间
-> 白名单判断
-> 鉴权
-> 租户和用户上下文写入 Header
-> 限流
-> 灰度路由
-> 路径重写
-> 转发下游
-> 记录状态码和耗时lb:// 转发的完整原理
很多人看到 uri: lb://asset-service 只知道“走负载均衡”,但不知道内部发生了什么。
flowchart TD
A["Route 命中 uri=lb://asset-service"] --> B["识别 scheme 是 lb"]
B --> C["ReactiveLoadBalancerClientFilter"]
C --> D["按 serviceId=asset-service 获取实例列表"]
D --> E["LoadBalancer 选择 ServiceInstance"]
E --> F["把目标 URI 改成 http://IP:Port"]
F --> G["NettyRoutingFilter 发起 HTTP 转发"]例如原始目标:
lb://asset-service/api/assets/100负载均衡后变成:
http://192.168.10.21:8080/api/assets/100所以 Gateway 503 常见不是“网关坏了”,而是:
| 原因 | 解释 |
|---|---|
| 服务名写错 | asset-service 在注册中心不存在 |
| 实例不健康 | 注册中心有服务但没有可用实例 |
| namespace/group 不一致 | 网关和服务不在同一个命名空间 |
| 灰度元数据过滤为空 | 路由规则要求 v2,但没有 v2 实例 |
| 本地缓存未刷新 | 服务刚上线或刚下线的短暂窗口 |
基础路由示例
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1含义:
- 请求路径匹配
/api/users/**。 - 转发到注册中心里的
user-service。 - 转发前去掉一层路径前缀。
常见 Predicate
| Predicate | 作用 |
|---|---|
| Path | 按路径匹配 |
| Method | 按 HTTP 方法匹配 |
| Header | 按请求头匹配 |
| Query | 按查询参数匹配 |
| Host | 按域名匹配 |
| Weight | 权重路由 |
Predicate 常见坑
Predicate 是“是否进入这条路由”的条件。多个 Predicate 同时存在时,通常是同时满足才命中。
spring:
cloud:
gateway:
routes:
- id: asset-write
uri: lb://asset-service
predicates:
- Path=/api/assets/**
- Method=POST,PUT,DELETE
- Header=X-Tenant-Id, .+这表示请求必须同时满足:
- 路径是
/api/assets/**。 - 方法是
POST、PUT或DELETE。 - 请求头里有
X-Tenant-Id。
常见坑:
| 坑 | 结果 |
|---|---|
Path 写成 /api/asset/** 少了 s | 请求不命中,返回 404 |
| Method 只写 GET | POST 请求进不来 |
| Header 正则太严格 | 正常请求被误拦 |
| 多条路由都能匹配 | 取决于路由顺序,可能走错服务 |
| Weight 分组写错 | 权重路由不生效 |
排查 404 时,第一步不是看下游 Controller,而是先确认 Gateway 是否命中 Route。
常见 Filter
| Filter | 作用 |
|---|---|
| StripPrefix | 去掉路径前缀 |
| AddRequestHeader | 添加请求头 |
| AddResponseHeader | 添加响应头 |
| RewritePath | 重写路径 |
| RequestRateLimiter | 限流 |
| CircuitBreaker | 熔断降级 |
路径重写为什么容易出错
路径重写是 Gateway 最常见 404 来源。
配置:
spring:
cloud:
gateway:
routes:
- id: asset-service
uri: lb://asset-service
predicates:
- Path=/api/assets/**
filters:
- StripPrefix=1客户端请求:
/api/assets/100StripPrefix=1 去掉第一段 /api 后,下游收到:
/assets/100如果下游 Controller 是:
@RestController
@RequestMapping("/api/assets")
public class AssetController {
@GetMapping("/{id}")
public AssetDTO get(@PathVariable Long id) {
return assetService.get(id);
}
}那就会 404,因为下游期望 /api/assets/100,但实际收到 /assets/100。
解决思路只有两个:
| 方案 | 做法 |
|---|---|
| 网关不去前缀 | 保持下游 Controller 使用 /api/assets |
| 下游不带公共前缀 | Controller 使用 /assets,网关统一去掉 /api |
一个团队里要统一风格,不要有的服务带 /api,有的服务不带,否则网关配置会越来越乱。
限流原理:令牌桶
Gateway 常用 RequestRateLimiter 配合 Redis 实现限流。它背后的思想通常可以用令牌桶理解:
flowchart TD
A["系统按固定速率生成令牌"] --> B["令牌放入桶"]
B --> C{"请求到达时桶里是否有令牌"}
C -- "有" --> D["取走令牌,请求放行"]
C -- "没有" --> E["请求被限流"]
B --> F{"桶是否已满"}
F -- "满" --> G["多余令牌丢弃"]令牌桶的好处是既能限制平均速率,也能允许一定突发流量。比如平时每秒生成 100 个令牌,桶容量 200,就允许短时间 200 个请求突发通过,但长期平均仍然受生成速率限制。
配置示例:
spring:
cloud:
gateway:
routes:
- id: asset-service
uri: lb://asset-service
predicates:
- Path=/api/assets/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
key-resolver: "#{@userKeyResolver}"key-resolver 决定按什么维度限流:按用户、IP、接口、租户都可以。医疗平台里通常会按医院编码、接口类型、租户和用户组合限流,避免某个医院接口异常重试拖垮整个平台。
限流 key 怎么设计
限流不是随便限。key 设计错了,会出现“该挡的没挡住,不该挡的被误伤”。
| key 维度 | 适合场景 | 风险 |
|---|---|---|
| IP | 防爬虫、匿名接口 | 公司出口 NAT 会误伤多人 |
| 用户 ID | 登录用户接口 | 一个租户内多个用户仍可能打爆系统 |
| 租户 ID | SaaS 多租户 | 大租户内部单用户异常不容易隔离 |
| 医院编码 | 医疗采集接口 | 同一医院多个接口互相影响 |
| 接口 + 租户 | 重要业务接口 | 规则较多,配置复杂 |
商业项目里常用组合 key:
limitKey = tenantId + ":" + apiCode例如:
@Bean
public KeyResolver tenantApiKeyResolver() {
return exchange -> {
String tenantId = exchange.getRequest().getHeaders().getFirst("X-Tenant-Id");
String path = exchange.getRequest().getPath().value();
return Mono.just((tenantId == null ? "anonymous" : tenantId) + ":" + path);
};
}注意:限流只是保护系统,不是权限控制。没有权限的请求应该 401/403,有权限但超过频率才是 429。
鉴权流程
网关常用于统一鉴权,但下游服务不能完全信任网关,关键权限仍要在服务端校验。
flowchart TD
A["请求进入 Gateway"] --> B["读取 Token"]
B --> C{"Token 是否存在"}
C -- "否" --> D["返回 401"]
C -- "是" --> E["校验 Token"]
E --> F{"是否有效"}
F -- "否" --> D
F -- "是" --> G["解析用户信息"]
G --> H["写入请求头"]
H --> I["转发下游服务"]商业系统鉴权 Demo
下面是一个简化版网关鉴权 Filter。它做三件事:白名单放行、校验 token、把用户信息透传给下游。
@Component
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final TokenService tokenService;
public AuthGlobalFilter(TokenService tokenService) {
this.tokenService = tokenService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getPath().value();
if (path.startsWith("/api/auth/login") || path.startsWith("/actuator/health")) {
return chain.filter(exchange);
}
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
UserPrincipal principal = tokenService.parse(token);
if (principal == null) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
ServerWebExchange newExchange = exchange.mutate()
.request(builder -> builder
.header("X-User-Id", principal.getUserId().toString())
.header("X-Tenant-Id", principal.getTenantId().toString()))
.build();
return chain.filter(newExchange);
}
@Override
public int getOrder() {
return -80;
}
}这里要理解一个安全边界:网关写入的 X-User-Id、X-Tenant-Id 只能被内部服务信任,不能让外部用户直接伪造。因此生产上要:
| 措施 | 目的 |
|---|---|
| 外部入口只允许访问 Gateway | 防止绕过网关直接调内部服务 |
| 网关转发前覆盖用户头 | 防止客户端伪造 X-User-Id |
| 内部服务仍校验关键权限 | 防止网关配置错误导致越权 |
| 敏感接口记录审计日志 | 追踪谁在什么时间访问了什么数据 |
CORS 跨域为什么经常出问题
跨域是浏览器安全策略,不是后端服务之间调用的问题。前端页面从 http://localhost:5173 调 http://api.xxx.com,浏览器会检查响应头是否允许跨域。
常见问题是:网关和下游都配置 CORS,导致响应头重复。
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: http://localhost:5173浏览器看到重复或冲突的 CORS 头,可能直接拦截。
建议:
| 场景 | 建议 |
|---|---|
| 前端统一走 Gateway | CORS 只在 Gateway 配 |
| 下游只给内部调用 | 下游不配 CORS |
| 携带 Cookie 或 Authorization | 不要简单使用 * |
| OPTIONS 预检失败 | 检查网关是否放行 OPTIONS |
Gateway 超时和下游超时怎么配合
Gateway 是入口,如果超时时间过长,会让入口连接堆积;如果过短,又会误杀正常慢请求。
flowchart TD
A["客户端请求"] --> B["Gateway 等待下游"]
B --> C{"超过 Gateway 超时?"}
C -->|"是"| D["返回 504 或降级"]
C -->|"否"| E["下游返回"]配置思路:
| 链路 | 建议 |
|---|---|
| Gateway -> 查询服务 | 短超时,例如 2 到 5 秒 |
| Gateway -> 文件上传 | 单独路由,超时和体积限制单独配置 |
| Gateway -> 外部慢接口 | 不建议同步透传,改异步任务 |
| Feign 内部调用 | 比 Gateway 更精细,按下游设置 |
不要让一个入口请求一直占住网关连接。像医疗数据采集、大批量导入、报表生成这类长任务,应该用“提交任务 -> 返回 taskId -> 前端轮询进度或 WebSocket 通知”的方式,而不是让 Gateway 同步等几十秒。
网关适合做什么
适合放在网关的能力:
- 统一路由。
- 登录态校验。
- 跨域处理。
- 限流。
- 请求日志。
- 链路追踪 ID。
- 灰度路由。
不适合放在网关的能力:
- 复杂业务规则。
- 订单计算。
- 数据库事务。
- 领域逻辑。
网关应该薄,业务应该在下游服务。
灰度发布和路由权重
Gateway 可以根据 Header、Cookie、权重等做灰度路由。
spring:
cloud:
gateway:
routes:
- id: asset-service-v1
uri: lb://asset-service-v1
predicates:
- Path=/api/assets/**
- Weight=asset, 90
- id: asset-service-v2
uri: lb://asset-service-v2
predicates:
- Path=/api/assets/**
- Weight=asset, 10含义:同一组 asset 中,约 90% 流量去 v1,10% 流量去 v2。实际灰度还要配合监控、回滚、版本标识和日志追踪,不能只配权重。
Header 灰度示例:
predicates:
- Path=/api/assets/**
- Header=X-Gray-Version, v2这样只有带 X-Gray-Version: v2 的请求才会进入新版本。
和 Nginx 的关系
常见部署链路:
用户 -> Nginx / Ingress -> Spring Cloud Gateway -> 微服务Nginx 更偏流量入口和静态资源,Gateway 更偏微服务路由、鉴权和业务上下文。
常见问题
404
检查 Path Predicate 是否匹配,StripPrefix 后下游路径是否正确。
503
检查下游服务是否注册到注册中心,服务名是否写对。
跨域失败
跨域配置要统一,不要网关和下游服务重复设置冲突的 CORS 响应头。
请求头丢失
检查 Filter 是否重写或删除了请求头,下游是否正确读取。
线上排查顺序
| 现象 | 优先看什么 |
|---|---|
| 404 | Route 是否匹配、Path 是否被 Rewrite/StripPrefix 改错 |
| 503 | 注册中心实例、服务名、下游健康状态 |
| 401/403 | Token 解析、权限规则、白名单配置 |
| 大量 429 | 限流 key、阈值、突发容量 |
| P99 延迟高 | 下游耗时、网关 Filter 是否阻塞、连接池 |
| Trace 断裂 | X-Trace-Id 是否在 Gateway 和 Feign 间透传 |
| 跨域失败 | CORS 响应头是否重复或冲突 |
排查网关问题不要只看网关日志,还要把网关 routeId、traceId、下游服务实例、响应码、耗时一起打出来。
商业落地原则
- 网关做入口治理,不写复杂领域业务。
- 所有下游转发都要有超时、熔断或限流策略。
- 认证可在网关统一做,但核心数据权限仍要下游校验。
- 灰度发布要能快速回滚。
- 过滤器里避免阻塞调用。
- 网关本身也要接入健康检查、指标和告警。
本章小结
Spring Cloud Gateway 是微服务统一入口。核心要掌握 Route、Predicate、Filter 三个概念,理解请求如何匹配、如何处理、如何转发。网关做通用横切能力,不要承载复杂业务。
