☰
Spring Cloud Gateway 快速入门:路由、过滤器与鉴权限流实战
2026/10/21 18:39:05 网站建设 项目流程

前面几篇文章把注册中心、配置中心这些微服务基础组件过了一遍,这次轮到 Spring Cloud Gateway。网关在微服务架构里的定位比较特殊——它不是业务必须,但服务一旦拆分到两个以上,对外统一入口就成了刚需。这篇快速入门会从路由原理讲到常见坑,目标是让一个没接触过网关的读者,跟着配置就能把请求从客户端转发到下游服务,并顺手加上鉴权和限流。

我会把版本匹配、依赖引入、过滤器写法这些容易卡住的地方全部摊开来讲。如果你已经在项目里用过其他网关产品,也可以对比着看,重点是理解 Gateway 这套响应式模型和下划线命名风格的路由配置。

1. 网关要解决的从来不只是"转发"这件事

1.1 没有网关时,客户端调用微服务有多痛苦

假设你有一个订单服务、一个用户服务、一个库存服务,拆完之后客户端怎么调?最原始的办法是客户端记三个地址,分别去请求。起先服务少,问题不明显,等实例数量涨起来就难受了:

第一,服务地址会变。实例扩缩容、机器迁移、环境切换,每次变动客户端都要跟着改。第二,鉴权逻辑重复。每个服务的接口都要验证登录态,写一遍还行,写三遍、五遍就是体力活。第三,横切需求无处安放。比如接口耗时统计、日志记录、灰度发布标识,这些逻辑放在每个服务里既臃肿又难维护。

网关这一层就是用来收口的。它作为请求入口,统一做路由转发、鉴权、限流、日志、跨域处理,下游服务只需要关心自己的业务。从架构演进顺序看,它通常是服务拆分到一定规模之后才出现的组件,但实际项目中往往是先拆服务,痛过几轮之后才回头补上网关。

1.2 为什么是 Spring Cloud Gateway 而不是 Zuul 1.x

老项目里用 Zuul 1.x 的不少,但新项目我基本不推荐。Zuul 1.x 基于 Servlet 模型,每个请求占一个线程,处理过程中线程一直阻塞等待下游返回。在高并发场景下,线程池一旦被打满,新请求就只能排队或者直接拒绝,表现就是服务入口雪崩。

Spring Cloud Gateway 基于 Spring WebFlux 和 Netty,走的是异步非阻塞模型。同样一台机器,面对大量慢下游、长连接场景,线程资源的占用会小很多。它处理请求时不再是一个请求占一个线程,而是用少量事件循环线程配合回调机制,吞吐量上限更高。

这里有一个很容易忽略的坑:Gateway 是响应式栈,不能和 spring-boot-starter-web 共存。如果你在网关项目里顺手引入了 Web 起步依赖,应用启动时会出现 Spring MVC 和 WebFlux 同时存在的诡异问题。严格说也不是完全不能启动,但路由不会按预期生效,各种 ClassCastException 会冒出来,徒增排查成本。

维度Zuul 1.xSpring Cloud Gateway
底层模型Servlet 阻塞 I/OWebFlux + Netty 非阻塞
线程模型每请求一线程事件循环 + 回调
长连接/高并发表现线程池容易耗尽资源占用相对稳定
与 Spring Boot 兼容性传统 Servlet 栈不能与 spring-boot-starter-web 共存

2. 依赖与版本:别让网关死在起跑线上

2.1 版本对应关系是第一个坑

Spring Cloud 的版本号体系本身就是一个容易懵的地方。它不像 Spring Boot 用数字小版本递进,而是用伦敦地铁站名作为版本名,比如 Hoxton、2020.0.x、2021.0.x。这不是重点,重点是它必须和 Spring Boot 的版本对齐,对不齐就会出现各种自相矛盾的依赖传递。

我自己常用的对应关系如下:

Spring BootSpring Cloud备注
2.4.x / 2.5.x2020.0.x(Ilford)老组合,仍可运行
2.6.x / 2.7.x2021.0.x(Jubilee)目前最稳妥的组合
3.0.x / 3.1.x2022.0.x(Kilburn)需要 JDK17,且是 javax 到 jakarta 的迁移

如果只是学习快速入门,我个人建议直接用 Spring Boot 2.7.x 搭配 Spring Cloud 2021.0.x。原因很简单:这套组合资料最多,排查问题的时候网上的答案基本都能对上号。如果你的生产环境已经上了 JDK17,那再考虑 2022.0.x,别在入门阶段同时引入两个大版本迁移的变量。

2.2 最小工程的 Maven 依赖长什么样

新建一个空 Spring Boot 工程,网关部分加一个核心依赖就行:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>

重点来了:不需要手动加 spring-boot-starter-web,也不需要加 WebFlux 的 starter,Gateway 的 starter 会把响应式栈需要的依赖都带进来。如果你后面要接入 Nacos 做服务发现和负载均衡,再加两个:

<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>

loadbalancer 这个依赖很关键。Spring Cloud 2020 版本之后移除了 Ribbon,lb:// 协议的负载均衡能力改由 spring-cloud-starter-loadbalancer 提供。很多人配了 lb:// 路由后一直报错找不到服务实例,十有八九是漏了这个依赖。

2.3 第一个路由配置与启动验证

先不接注册中心,用一个最简单的静态路由把网关跑起来。在 application.yml 里写:

server: port: 8080 spring: application: name: gateway-server cloud: gateway: routes: - id: demo-static-route uri: http://127.0.0.1:8081 predicates: - Path=/demo/**

这段配置的意思是:所有访问网关 8080 端口、路径以 /demo/ 开头的请求,全部转发给 127.0.0.1:8081,并且路径原样带过去。启动网关后,用 curl 验证:

curl http://localhost:8080/demo/hello

如果下游 8081 服务收到了 /demo/hello 这个请求,说明网关最基础的链路已经通了。这一步跑通的意义在于,先把 Gateway 本身的复杂性隔离掉,后面再做服务发现、过滤器,心里就有底了。

3. 路由三要素:Predicate、Filter、Route 的实际协作方式

3.1 一张路由表就是一个"如果那么"决策表

Gateway 里三个最核心的概念是 Route、Predicate、Filter。它们的关系用一个生活化的类比就能讲清楚。

把网关想象成一个快递分拣中心。一条 Route 就是一条分拣规则:如果包裹满足某个条件,就把它送往某个目的地。Predicate 就是那个条件判断,比如"面单上写着北京的就走北京通道";Filter 则是包裹在运输前后被做的加工,比如贴个标签、量一下尺寸、或者拦截掉疑似违禁品。

一条完整路由配置就是:

spring: cloud: gateway: routes: - id: route-a uri: http://backend-a:9000 predicates: - Path=/a/** filters: - StripPrefix=1

这段配置的含义是:凡是路径以 /a/ 开头的请求,都转发到 http://backend-a:9000,并且转发前把第一段路径前缀 /a 去掉。比如客户端请求 /a/user/list,下游服务实际收到的是 /user/list。这个细节非常重要,很多前后端联调对不上路径,问题就出在 StripPrefix 用没用对。

3.2 请求进来后网关内部做了什么

一次请求到网关,经历的链条大致是这样的:

请求先被 Netty 接收,进入 DispatcherHandler;Gateway 的 GatewayHandlerMapping 会遍历所有路由,用每个路由的 Predicate 集合去匹配当前请求。匹配成功后,拿出该路由下面配置的所有 Filter,拼成一个过滤器链;链上最后一个过滤器会通过 WebClient 把请求转发到下游服务。下游响应回来后,再沿着过滤器链的返回路径逐层加工,最终写回客户端。

这里有个概念值得记住:ServerWebExchange。它相当于一次请求的上下文对象,封装了请求和响应。写自定义过滤器的时候,你想拿到请求头、请求参数、设置响应状态码,全部都得通过它操作。它是 Gateway 过滤器编程里出现频率最高的东西。

3.3 uri 的三种写法与选择时机

uri 的写法决定了 Gateway 把请求交给谁,三种写法对应三种场景:

第一种是静态地址,直接写 http://ip:port,适合下游服务固定、没有注册中心的情况,也是入门阶段最推荐的写法。第二种是 lb://service-name,配合注册中心和 LoadBalancer 使用,下游服务可以多实例动态扩缩容,Gateway 会自动做负载均衡。第三种是 ws:// 或 wss://,用于转发 WebSocket 请求。

入门的时候我建议先把静态地址玩熟,再升级到 lb://。因为 lb:// 会引入注册中心、服务名解析、负载均衡策略这几个新变量,一旦出现问题,你很难判断到底是路由写错了还是服务没注册成功。

4. 路由实战:从静态转发一路做到服务发现

4.1 先用静态 URI 把链路跑通

假设你在本地有一个订单服务,端口 8081,接口路径是 /order/list。现在要让网关把 /api/order/** 的请求转给它,同时去掉 /api 前缀,配置可以这样写:

spring: cloud: gateway: routes: - id: order-service uri: http://127.0.0.1:8081 predicates: - Path=/api/order/** filters: - StripPrefix=1

请求 /api/order/list 到达网关后,StripPrefix=1 会剥掉第一段 /api,下游收到的是 /order/list。这里我踩过的一个典型错误是:明明下游接口是 /order/list,结果漏配了 StripPrefix,下游一直报 404,查了半天才发现网关把 /api/order/list 整个路径原样转发过去了。

静态路由的调试思路也简单:在下游服务打印请求路径,或者在网关端打开日志,看一下实际转发的 URI 就能定位问题。网关日志级别可以这样调:

logging: level: org.springframework.cloud.gateway: debug

打开之后能看到路由匹配过程、过滤器链的组装情况,对学习 Gateway 非常有帮助。

4.2 接入注册中心后改用 lb:// 协议

服务多了,静态地址就维护不动了。接入注册中心之后,路由改成服务名方式:

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

lb://order-service 的含义是:去注册中心拉取名为 order-service 的服务实例列表,然后根据负载均衡策略选一个实例发起转发。Gateway 默认的负载均衡策略是轮询,也可以通过配置调整加权或自定义策略。

这里的硬性前提有两个:一是网关本身必须注册到同一个注册中心,或者至少能访问注册中心的服务发现接口;二是工程里必须引入 spring-cloud-starter-loadbalancer,否则 lb:// 报错。我遇到过不止一次,服务注册正常,控制台能查到实例,但路由一直 503,最后发现是少了一个依赖的事。

4.3 StripPrefix 与 Path 的配合细节

StripPrefix 的数字不是随便填的,它代表要去掉几层路径前缀。路径 /api/v1/order/list,StripPrefix=1 结果是 /v1/order/list,StripPrefix=2 结果是 /order/list。这和 RewritePath 是两种思路:StripPrefix 按层数切,RewritePath 按正则换。

filters: - StripPrefix=2

或

filters: - RewritePath=/api/(?<segment>.*), /$\{segment}

RewritePath 的配置里有一个容易让新手困惑的点:第二个参数里写 ${segment} 时,在 YAML 文件中需要写成 ${segment},否则会被 Spring 的属性占位符机制提前解析掉,导致路由无法启动。这个坑我在第五个章节还会再提一次,因为它确实隐蔽。

我的实际建议是:能统一 Restful 规范就统一,比如网关前缀全部是一层 /api,内部服务路径全部是 /order、/user 这种,那用 StripPrefix=1 就最省心。不要一个服务一套路径风格,不然路由配置会变成一场灾难。

5. 断言工厂与过滤器工厂的常用组合

5.1 最常用的 7 个断言工厂

Predicate 工厂是 Spring Cloud Gateway 里对"条件判断"能力的封装。除了 Path,还有几个高频用法值得掌握:

Predicate 工厂作用示例
Path按路径匹配Path=/api/**
Method按 HTTP 方法匹配Method=GET,POST
Query按查询参数匹配Query=foo 或 Query=foo,bar
Header按请求头匹配Header=X-Request-Id,\d+
Cookie按 Cookie 匹配Cookie=sessionId,abc
RemoteAddr按来源 IP 匹配RemoteAddr=192.168.1.1/24
After/Before/Between按时间窗匹配After=2024-01-01T00:00:00+08:00

多条 Predicate 写在同一路由下时,它们之间是 AND 关系,必须全部满足才会匹配。比如:

predicates: - Path=/order/** - Method=POST - Header=X-Order-Version,v2

这条路由只会匹配 POST /order/** 且请求头 X-Order-Version 等于 v2 的请求。利用这个特性,你可以做出简单的多版本路由:不同版本号走不同下游服务。

5.2 常用过滤器工厂与重试风险

Gateway 内置了几十个 Filter 工厂,我的经验是最常用的是下面这几个:

Filter 工厂作用典型配置
StripPrefix剥掉路径前缀StripPrefix=1
AddRequestHeader附加请求头AddRequestHeader=X-Source,gateway
AddResponseHeader附加响应头AddResponseHeader=X-Server,gateway
RewritePath正则重写路径RewritePath=/api/(? .*),/${seg}
Retry下游请求失败重试Retry=retries:3,statuses:502

Retry 这个过滤器要特别谨慎。它看起来是增强可靠性,实际用不好会给下游造成翻倍的请求压力。我见过一个项目在网关注入层配置了 Retry,下游某个接口一旦响应超时,网关会立刻重试,结果把本来就吃紧的下游直接打挂。重试只适合幂等接口,而且最好配合超时熔断一起用,别把它当成万能药。

5.3 RequestRateLimiter:网关限流的入门姿势

网关层做限流最常用的方案是 RequestRateLimiter,它基于 Redis + Token Bucket 令牌桶算法实现。配置分两块:一是路由里的过滤器配置,二是定义一个 KeyResolver 的 Bean,用来决定以什么维度限流,比如按用户 ID、按 IP、按某个请求参数。

filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: "#{@ipKeyResolver}"

replenishRate 是每秒向桶里补充的令牌数,也就是平均每秒允许请求数;burstCapacity 是桶容量,也就是允许的瞬时突发请求数。对应到现实,10/20 的意思是平均每秒 10 个请求,突发情况下最多连续放过 20 个。

对应 KeyResolver:

@Bean public KeyResolver ipKeyResolver() { return exchange -> { String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); return Mono.just(ip); }; }

需要提醒的是,RequestRateLimiter 依赖 Redis 运行,本地没起 Redis 的话,这个过滤器会在运行时直接报错,路由本身都不会生效。快速入门阶段如果只想看限流效果,先把 Redis 跑起来再配。

6. 自定义全局过滤器:把统一鉴权收口到网关

6.1 为什么要把鉴权放在网关层

如果把鉴权放在每个下游服务里,你会发现代码高度重复:每个服务的拦截器都要校验 token,都要调用户中心换取用户信息。一旦鉴权逻辑调整,比如从 JWT 换成 OAuth2,所有服务都要跟着改一遍。

放到网关层之后,下游服务只需要信任网关转发的请求头里携带的用户信息即可。网关在转发前统一校验 token,校验通过后把用户身份透传给下游,下游直接使用。这样鉴权逻辑只维护一份,审计日志、黑名单拦截、接口流量统计这些横切逻辑,也都可以集中到网关处理。

6.2 写一个最简单的 Token 校验过滤器

Spring Cloud Gateway 提供 GlobalFilter 接口,实现它可以写出作用于全路由的过滤器。下面这个过滤器会校验请求头里的 token,等于预设值才放行,否则直接返回 401:

@Component public class AuthGlobalFilter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token = exchange.getRequest() .getHeaders() .getFirst("token"); // 这里仅作示例,实际项目中需要调用认证服务校验 if (token == null || !"gateway-token".equals(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); } @Override public int getOrder() { return -100; } }

getOrder() 返回值决定了过滤器在过滤链中的执行顺序,值越小优先级越高。负号在这里就是刻意让它排在大多数内置过滤器之前。若返回值是 0 或正数,执行时机可能靠后,某些内置过滤器已经提前做了转发决策,你的鉴权逻辑就失去了拦截意义。

判断校验失败时,exchange.getResponse().setComplete() 会直接结束当前请求,不再执行后续过滤器,也不会转发到下游。返回值是 Mono ,这是响应式编程的写法,刚接触的话理解成"声明这个操作完成"就行。

6.3 鉴权通过后如何把用户信息透传给下游

校验通过后,常见需求是把用户 ID 或者角色信息带给下游服务。做法是修改请求上下文里的请求头,再继续执行过滤器链:

@Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String userId = resolveUserId(exchange); ServerWebExchange mutatedExchange = exchange.mutate() .request(builder -> builder.header("X-User-Id", userId)) .build(); return chain.filter(mutatedExchange); }

关键点是 exchange.mutate() 生成新的 ServerWebExchange,然后让后续过滤器链使用这个新对象。如果你只是从原始 exchange 里拿 request 修改,改动不会生效。透传的请求头最好加个前缀约定,比如 X-User-Id、X-User-Name,避免和普通业务请求头冲突,下游也得配合约定只信任网关透传的这些头。

这里还有一个安全细节:如果下游服务直接暴露在公网上,或者有别的途经可以绕过网关访问,那"下游信任 X-User-Id 请求头"的策略就形同虚设了。网关鉴权只解决入口统一的问题,服务间的安全隔离还得靠网络策略和调用链路身份认证兜底。

7. 第一次跑网关最容易翻车的五个点

7.1 spring-boot-starter-web 和 WebFlux 的冲突

新人在搭建网关工程时,习惯性会勾选 Spring Web 依赖,结果 Gateway 启动后路由始终不生效,甚至报出 DispatcherServlet 相关的初始化错误。原因是 Gateway 的 starter 内部用的是 Spring WebFlux 栈,你一旦引入 spring-boot-starter-web,启动器会同时尝试配置 Spring MVC 的 DispatcherServlet 和 WebFlux 的 DispatcherHandler,两套 Web 框架共存在同一上下文里,行为完全不可预期。

排查方法也简单:打开 Maven 依赖树,看看有没有 spring-boot-starter-web,有就剔除掉。如果你确实需要 MVC 相关工具类,只引所需的最小依赖,别整个 web starter 拖进来。

7.2 lb:// 路由找不到服务实例

表现是请求返回 503 SERVICE UNAVAILABLE,日志里能看到类似 "Unable to find instance for order-service" 的提示。先确认三件事:服务名是否写错;网关是否依赖了 spring-cloud-starter-loadbalancer;网关是否配置了正确的注册中心地址。

还有一个容易忽略的细节:lb://order-service 里的服务名,必须和注册中心里服务注册名完全一致。比如服务端 application.yml 里写的是 spring.application.name: order-service,那路由里就写 lb://order-service,大小写和短横线都不能差。

7.3 跨域配置:网关层CORS与浏览器安全策略

前后端分离项目,前端页面在一个域名,接口走网关另一个域名,浏览器会发起 CORS 预检请求。如果网关没有配置跨域,前端会一直报 CORS error,但接口本身用 Postman 调又是通的,特别容易让人怀疑是前端问题。

Gateway 是在网关层统一处理跨域:

spring: cloud: gateway: globalcors: corsConfigurations: '[/**]': allowedOrigins: "*" allowedMethods: - GET - POST - PUT - DELETE - OPTIONS allowedHeaders: "*"

注意和生产环境尽量别用 allowedOrigins: "*",最好只放开公司内部前端域名的白名单。加了全局跨域配置后,如果路由里也配置了 CORS 相关的自定义过滤器,两者可能会叠加出奇怪的响应头,排查的时候记得把两处都看一眼。

7.4 超时、重试的默认行为与调整方式

Gateway 转发下游用的是 Netty 的 HttpClient,默认连接超时和响应超时有自己的初值,在慢接口场景下很可能被误触发。调整方式如下:

spring: cloud: gateway: httpclient: connect-timeout: 3000 response-timeout: 10s

connect-timeout 是建立连接的超时时间,单位毫秒;response-timeout 是等待响应的时间,可以带单位。注意 response-timeout 和 connect-timeout 是两码事,前者包含从发起请求到拿到完整响应的时间,后者只覆盖 TCP 连接建立阶段。

另外说一下全局重试默认值。Gateway 自带的重试过滤器在不配置时不会启用,但一旦你配置了 Retry 过滤器,要特别注意它会把哪些异常纳入重试。默认配置下 5xx 状态码和部分 IO 异常都会触发重试,对非幂等接口影响很大,具体配置要结合下游接口特性来定。

7.5 别在网关里用 RestTemplate 和 Feign

网关是 WebFlux 环境,你往里面注入 RestTemplate、FeignClient,虽然代码能写出来,但调用时会发现线程模型不匹配,要么阻塞了事件循环线程,要么依赖了 Servlet 相关 API 直接报错。在响应式应用里调用下游,应该用 WebClient 或者遵循响应式方式的调用组件。

如果你只是想在 Gateway 的路由逻辑之外额外调用某个第三方接口,比如鉴权服务校验 token,那应该注入 WebClient 构建一个非阻塞调用。这是从传统 Servlet 转向 Gateway 时最容易产生的不适感来源,习惯之后会发现它的模型其实更契合网关这种高并发、短业务逻辑的场景。

最后分享一个我自己的体会:网关上手阶段不必追求把所有内置工厂都记住,先把路由三要素和过滤器链的执行顺序吃透,剩下的都是查文档就能解决的问题。真正值得多花心思的是路由命名规范和过滤器执行顺序的统一约定,因为这种东西一旦在多个项目里铺开,后面再改就是牵一发动全身的事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询