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. 方法是 POSTPUTDELETE
  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-IdX-Tenant-Id 只能被内部服务信任,不能让外部用户直接伪造。因此生产上要:

措施目的
外部入口只允许访问 Gateway防止绕过网关直接调内部服务
网关转发前覆盖用户头防止客户端伪造 X-User-Id
内部服务仍校验关键权限防止网关配置错误导致越权
敏感接口记录审计日志追踪谁在什么时间访问了什么数据

CORS 跨域为什么经常出问题

跨域是浏览器安全策略,不是后端服务之间调用的问题。前端页面从 http://localhost:5173http://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 三个概念,理解请求如何匹配、如何处理、如何转发。网关做通用横切能力,不要承载复杂业务。