Skip to content

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 是入口控制面,不应承载订单、支付等领域业务。

mermaid
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["响应过滤并返回"]

零基础可以把网关理解成系统的“大门”:客户端先访问网关,网关再根据规则把请求转发到后端服务。

为什么需要网关 ​

微服务系统里可能有很多服务:

  1. 用户服务。
  2. 文章服务。
  3. 评论服务。
  4. 文件服务。
  5. 搜索服务。

如果客户端直接访问每个服务,会有很多问题:

  1. 客户端要记住多个服务地址。
  2. 每个服务都要重复做鉴权。
  3. 跨域、限流、日志重复实现。
  4. 服务地址变化会影响客户端。

网关可以把这些通用能力集中处理。

请求流程 ​

mermaid
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 调用,就会把网关线程拖住,入口吞吐会明显下降。

mermaid
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。

mermaid
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。

mermaid
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 转发请求”,而是:

text
路由定义加载
  -> 请求进入 WebFlux
  -> RoutePredicateHandlerMapping 找路由
  -> FilteringWebHandler 执行过滤器链
  -> NettyRoutingFilter 转发请求
  -> NettyWriteResponseFilter 写回响应

一次请求在 Gateway 内部怎么走 ​

下面把一次请求拆细。以 /api/assets/100 转发到 lb://asset-service 为例:

mermaid
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
AttributesrouteId、目标 URI、traceId 等中间状态
Principal登录用户信息

很多 Gateway 原理都围绕 exchange 展开:过滤器读取请求、修改请求、写入属性、决定是否继续 chain.filter(exchange)。

Filter 链为什么有前置和后置 ​

Gateway Filter 的代码看起来只有一个 filter 方法,但因为 Reactor 的链式回调,它天然可以做“请求前”和“响应后”两段逻辑。

java
return chain.filter(exchange)
        .then(Mono.fromRunnable(() -> {
            // 这里是响应回来之后执行
        }));

执行顺序类似洋葱模型:

mermaid
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),请求就不会继续转发:

java
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();

这就是鉴权失败、限流失败可以直接在网关返回 401/429 的原因。

Filter 的前置和后置 ​

Gateway Filter 不是只有请求前处理,也可以在响应回来后处理。

java
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 做了两件事:

  1. 请求前:补充 X-Trace-Id。
  2. 响应后:记录网关耗时。

实际生产中应使用日志框架,不要用 System.out.println。

Filter 顺序为什么重要 ​

多个 Filter 同时存在时,顺序不对会直接改变行为。

顺序问题后果
日志 Filter 在 traceId 生成前执行日志里没有 traceId
鉴权 Filter 在白名单判断后执行错误公开接口被拦截或私有接口放行
StripPrefix 在 RewritePath 后顺序不对下游路径错乱,出现 404
限流在鉴权前按匿名 IP 做 key同一出口用户互相影响
响应包装在文件下载后执行文件流被破坏

建议顺序:

text
生成 traceId
  -> 记录访问日志开始时间
  -> 白名单判断
  -> 鉴权
  -> 租户和用户上下文写入 Header
  -> 限流
  -> 灰度路由
  -> 路径重写
  -> 转发下游
  -> 记录状态码和耗时

lb:// 转发的完整原理 ​

很多人看到 uri: lb://asset-service 只知道“走负载均衡”,但不知道内部发生了什么。

mermaid
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 转发"]

例如原始目标:

text
lb://asset-service/api/assets/100

负载均衡后变成:

text
http://192.168.10.21:8080/api/assets/100

所以 Gateway 503 常见不是“网关坏了”,而是:

原因解释
服务名写错asset-service 在注册中心不存在
实例不健康注册中心有服务但没有可用实例
namespace/group 不一致网关和服务不在同一个命名空间
灰度元数据过滤为空路由规则要求 v2,但没有 v2 实例
本地缓存未刷新服务刚上线或刚下线的短暂窗口

基础路由示例 ​

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/users/**
          filters:
            - StripPrefix=1

含义:

  1. 请求路径匹配 /api/users/**。
  2. 转发到注册中心里的 user-service。
  3. 转发前去掉一层路径前缀。

常见 Predicate ​

Predicate作用
Path按路径匹配
Method按 HTTP 方法匹配
Header按请求头匹配
Query按查询参数匹配
Host按域名匹配
Weight权重路由

Predicate 常见坑 ​

Predicate 是“是否进入这条路由”的条件。多个 Predicate 同时存在时,通常是同时满足才命中。

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: asset-write
          uri: lb://asset-service
          predicates:
            - Path=/api/assets/**
            - Method=POST,PUT,DELETE
            - Header=X-Tenant-Id, .+

这表示请求必须同时满足:

  1. 路径是 /api/assets/**。
  2. 方法是 POST、PUT 或 DELETE。
  3. 请求头里有 X-Tenant-Id。

常见坑:

坑结果
Path 写成 /api/asset/** 少了 s请求不命中,返回 404
Method 只写 GETPOST 请求进不来
Header 正则太严格正常请求被误拦
多条路由都能匹配取决于路由顺序,可能走错服务
Weight 分组写错权重路由不生效

排查 404 时,第一步不是看下游 Controller,而是先确认 Gateway 是否命中 Route。

常见 Filter ​

Filter作用
StripPrefix去掉路径前缀
AddRequestHeader添加请求头
AddResponseHeader添加响应头
RewritePath重写路径
RequestRateLimiter限流
CircuitBreaker熔断降级

路径重写为什么容易出错 ​

路径重写是 Gateway 最常见 404 来源。

配置:

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: asset-service
          uri: lb://asset-service
          predicates:
            - Path=/api/assets/**
          filters:
            - StripPrefix=1

客户端请求:

text
/api/assets/100

StripPrefix=1 去掉第一段 /api 后,下游收到:

text
/assets/100

如果下游 Controller 是:

java
@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 实现限流。它背后的思想通常可以用令牌桶理解:

mermaid
flowchart TD
    A["系统按固定速率生成令牌"] --> B["令牌放入桶"]
    B --> C{"请求到达时桶里是否有令牌"}
    C -- "有" --> D["取走令牌,请求放行"]
    C -- "没有" --> E["请求被限流"]
    B --> F{"桶是否已满"}
    F -- "满" --> G["多余令牌丢弃"]

令牌桶的好处是既能限制平均速率,也能允许一定突发流量。比如平时每秒生成 100 个令牌,桶容量 200,就允许短时间 200 个请求突发通过,但长期平均仍然受生成速率限制。

配置示例:

yaml
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登录用户接口一个租户内多个用户仍可能打爆系统
租户 IDSaaS 多租户大租户内部单用户异常不容易隔离
医院编码医疗采集接口同一医院多个接口互相影响
接口 + 租户重要业务接口规则较多,配置复杂

商业项目里常用组合 key:

text
limitKey = tenantId + ":" + apiCode

例如:

java
@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。

鉴权流程 ​

网关常用于统一鉴权,但下游服务不能完全信任网关,关键权限仍要在服务端校验。

mermaid
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、把用户信息透传给下游。

java
@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,导致响应头重复。

text
Access-Control-Allow-Origin: *
Access-Control-Allow-Origin: http://localhost:5173

浏览器看到重复或冲突的 CORS 头,可能直接拦截。

建议:

场景建议
前端统一走 GatewayCORS 只在 Gateway 配
下游只给内部调用下游不配 CORS
携带 Cookie 或 Authorization不要简单使用 *
OPTIONS 预检失败检查网关是否放行 OPTIONS

Gateway 超时和下游超时怎么配合 ​

Gateway 是入口,如果超时时间过长,会让入口连接堆积;如果过短,又会误杀正常慢请求。

mermaid
flowchart TD
    A["客户端请求"] --> B["Gateway 等待下游"]
    B --> C{"超过 Gateway 超时?"}
    C -->|"是"| D["返回 504 或降级"]
    C -->|"否"| E["下游返回"]

配置思路:

链路建议
Gateway -> 查询服务短超时,例如 2 到 5 秒
Gateway -> 文件上传单独路由,超时和体积限制单独配置
Gateway -> 外部慢接口不建议同步透传,改异步任务
Feign 内部调用比 Gateway 更精细,按下游设置

不要让一个入口请求一直占住网关连接。像医疗数据采集、大批量导入、报表生成这类长任务,应该用“提交任务 -> 返回 taskId -> 前端轮询进度或 WebSocket 通知”的方式,而不是让 Gateway 同步等几十秒。

网关适合做什么 ​

适合放在网关的能力:

  1. 统一路由。
  2. 登录态校验。
  3. 跨域处理。
  4. 限流。
  5. 请求日志。
  6. 链路追踪 ID。
  7. 灰度路由。

不适合放在网关的能力:

  1. 复杂业务规则。
  2. 订单计算。
  3. 数据库事务。
  4. 领域逻辑。

网关应该薄,业务应该在下游服务。

灰度发布和路由权重 ​

Gateway 可以根据 Header、Cookie、权重等做灰度路由。

yaml
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 灰度示例:

yaml
predicates:
  - Path=/api/assets/**
  - Header=X-Gray-Version, v2

这样只有带 X-Gray-Version: v2 的请求才会进入新版本。

和 Nginx 的关系 ​

常见部署链路:

text
用户 -> Nginx / Ingress -> Spring Cloud Gateway -> 微服务

Nginx 更偏流量入口和静态资源,Gateway 更偏微服务路由、鉴权和业务上下文。

常见问题 ​

404 ​

检查 Path Predicate 是否匹配,StripPrefix 后下游路径是否正确。

503 ​

检查下游服务是否注册到注册中心,服务名是否写对。

跨域失败 ​

跨域配置要统一,不要网关和下游服务重复设置冲突的 CORS 响应头。

请求头丢失 ​

检查 Filter 是否重写或删除了请求头,下游是否正确读取。

线上排查顺序 ​

现象优先看什么
404Route 是否匹配、Path 是否被 Rewrite/StripPrefix 改错
503注册中心实例、服务名、下游健康状态
401/403Token 解析、权限规则、白名单配置
大量 429限流 key、阈值、突发容量
P99 延迟高下游耗时、网关 Filter 是否阻塞、连接池
Trace 断裂X-Trace-Id 是否在 Gateway 和 Feign 间透传
跨域失败CORS 响应头是否重复或冲突

排查网关问题不要只看网关日志,还要把网关 routeId、traceId、下游服务实例、响应码、耗时一起打出来。

商业落地原则 ​

  1. 网关做入口治理,不写复杂领域业务。
  2. 所有下游转发都要有超时、熔断或限流策略。
  3. 认证可在网关统一做,但核心数据权限仍要下游校验。
  4. 灰度发布要能快速回滚。
  5. 过滤器里避免阻塞调用。
  6. 网关本身也要接入健康检查、指标和告警。

本章小结 ​

Spring Cloud Gateway 是微服务统一入口。核心要掌握 Route、Predicate、Filter 三个概念,理解请求如何匹配、如何处理、如何转发。网关做通用横切能力,不要承载复杂业务。