Spring Cloud Gateway 快速入门:路由、过滤器与鉴权限流实战
发布时间:2026/10/21 18:39:11 锦皓数字建站

前面几篇文章把注册中心、配置中心这些微服务基础组件过了一遍这次轮到 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.xIlford老组合仍可运行2.6.x / 2.7.x2021.0.xJubilee目前最稳妥的组合3.0.x / 3.1.x2022.0.xKilburn需要 JDK17且是 javax 到 jakarta 的迁移如果只是学习快速入门我个人建议直接用 Spring Boot 2.7.x 搭配 Spring Cloud 2021.0.x。原因很简单这套组合资料最多排查问题的时候网上的答案基本都能对上号。如果你的生产环境已经上了 JDK17那再考虑 2022.0.x别在入门阶段同时引入两个大版本迁移的变量。2.2 最小工程的 Maven 依赖长什么样新建一个空 Spring Boot 工程网关部分加一个核心依赖就行dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency重点来了不需要手动加 spring-boot-starter-web也不需要加 WebFlux 的 starterGateway 的 starter 会把响应式栈需要的依赖都带进来。如果你后面要接入 Nacos 做服务发现和负载均衡再加两个dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependency dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependencyloadbalancer 这个依赖很关键。Spring Cloud 2020 版本之后移除了 Ribbonlb:// 协议的负载均衡能力改由 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: - StripPrefix1这段配置的含义是凡是路径以 /a/ 开头的请求都转发到 http://backend-a:9000并且转发前把第一段路径前缀 /a 去掉。比如客户端请求 /a/user/list下游服务实际收到的是 /user/list。这个细节非常重要很多前后端联调对不上路径问题就出在 StripPrefix 用没用对。3.2 请求进来后网关内部做了什么一次请求到网关经历的链条大致是这样的请求先被 Netty 接收进入 DispatcherHandlerGateway 的 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: - StripPrefix1请求 /api/order/list 到达网关后StripPrefix1 会剥掉第一段 /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: - StripPrefix1lb://order-service 的含义是去注册中心拉取名为 order-service 的服务实例列表然后根据负载均衡策略选一个实例发起转发。Gateway 默认的负载均衡策略是轮询也可以通过配置调整加权或自定义策略。这里的硬性前提有两个一是网关本身必须注册到同一个注册中心或者至少能访问注册中心的服务发现接口二是工程里必须引入 spring-cloud-starter-loadbalancer否则 lb:// 报错。我遇到过不止一次服务注册正常控制台能查到实例但路由一直 503最后发现是少了一个依赖的事。4.3 StripPrefix 与 Path 的配合细节StripPrefix 的数字不是随便填的它代表要去掉几层路径前缀。路径 /api/v1/order/listStripPrefix1 结果是 /v1/order/listStripPrefix2 结果是 /order/list。这和 RewritePath 是两种思路StripPrefix 按层数切RewritePath 按正则换。filters: - StripPrefix2或filters: - RewritePath/api/(?segment.*), /$\{segment}RewritePath 的配置里有一个容易让新手困惑的点第二个参数里写 ${segment} 时在 YAML 文件中需要写成 ${segment}否则会被 Spring 的属性占位符机制提前解析掉导致路由无法启动。这个坑我在第五个章节还会再提一次因为它确实隐蔽。我的实际建议是能统一 Restful 规范就统一比如网关前缀全部是一层 /api内部服务路径全部是 /order、/user 这种那用 StripPrefix1 就最省心。不要一个服务一套路径风格不然路由配置会变成一场灾难。5. 断言工厂与过滤器工厂的常用组合5.1 最常用的 7 个断言工厂Predicate 工厂是 Spring Cloud Gateway 里对条件判断能力的封装。除了 Path还有几个高频用法值得掌握Predicate 工厂作用示例Path按路径匹配Path/api/**Method按 HTTP 方法匹配MethodGET,POSTQuery按查询参数匹配Queryfoo 或 Queryfoo,barHeader按请求头匹配HeaderX-Request-Id,\dCookie按 Cookie 匹配CookiesessionId,abcRemoteAddr按来源 IP 匹配RemoteAddr192.168.1.1/24After/Before/Between按时间窗匹配After2024-01-01T00:00:0008:00多条 Predicate 写在同一路由下时它们之间是 AND 关系必须全部满足才会匹配。比如predicates: - Path/order/** - MethodPOST - HeaderX-Order-Version,v2这条路由只会匹配 POST /order/** 且请求头 X-Order-Version 等于 v2 的请求。利用这个特性你可以做出简单的多版本路由不同版本号走不同下游服务。5.2 常用过滤器工厂与重试风险Gateway 内置了几十个 Filter 工厂我的经验是最常用的是下面这几个Filter 工厂作用典型配置StripPrefix剥掉路径前缀StripPrefix1AddRequestHeader附加请求头AddRequestHeaderX-Source,gatewayAddResponseHeader附加响应头AddResponseHeaderX-Server,gatewayRewritePath正则重写路径RewritePath/api/(? .*),/${seg}Retry下游请求失败重试Retryretries:3,statuses:502Retry 这个过滤器要特别谨慎。它看起来是增强可靠性实际用不好会给下游造成翻倍的请求压力。我见过一个项目在网关注入层配置了 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 个。对应 KeyResolverBean 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等于预设值才放行否则直接返回 401Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid 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 MonoVoid 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: 10sconnect-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 时最容易产生的不适感来源习惯之后会发现它的模型其实更契合网关这种高并发、短业务逻辑的场景。最后分享一个我自己的体会网关上手阶段不必追求把所有内置工厂都记住先把路由三要素和过滤器链的执行顺序吃透剩下的都是查文档就能解决的问题。真正值得多花心思的是路由命名规范和过滤器执行顺序的统一约定因为这种东西一旦在多个项目里铺开后面再改就是牵一发动全身的事。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。