
我接手过好几个从单体往微服务拆的项目每次拆到十几二十个服务的时候第一个炸的点永远是认证和鉴权——每个服务都得塞一段 JWT 校验逻辑CORS 到处配日志格式五花八门线上出个问题要顺着服务链 Log 翻半天。后来我们把 Spring Cloud Gateway 架在最前面路由、统一认证、限流、熔断全部收敛到网关这一层整个体系的混乱程度才真正降了下来。这篇东西不是官网文档的翻译是我在几个企业级项目里把 Spring Cloud Gateway 从零搭到生产的全过程记录包括路由怎么配才能少踩坑、统一认证的 Filter 链路怎么设计、限流熔断的参数怎么调才不会被误伤。适合正在搭微服务网关、或者已经在用网关但总感觉哪里不对劲的团队参考。1. 拆服务拆出来的痛点网关到底解决了什么问题1.1 没有网关之前微服务是什么样子先还原一个典型场景。你有一个用户服务、一个订单服务、一个支付服务、一个商品服务四个服务独立部署前端直接分别调用它们的地址。表面看没什么问题但用起来全是事。第一个问题是认证逻辑散落各处。最初可能只有用户服务校验 Token后来订单服务也要登录才能访问再后来支付服务要验权限。每个团队写自己那份认证代码有的用 Shiro有的用 Spring Security有的直接手写拦截器。结果是同一个 Token在不同服务里的校验逻辑居然不一样——用户在订单服务能过在支付服务就被拦下来查半天发现是两套 JWT 密钥压根不是同一个。第二个问题是 CORS 和跨域配置混乱。前端要调四个服务每个服务都得配允许跨域配漏一个前端就报错而且报错信息非常误导人经常被当成后端接口问题排查半天。第三个问题是流量入口不可控。没有统一入口就没办法做全局限流。某个服务被刷爆了只能去改那个服务的代码加上限流逻辑再重新发布。等你发完流量已经打垮了。网关的本质就是把每个服务都要重复做一遍的这些事——路由、认证、限流、熔断、跨域、日志——全部抽出来放到一个独立组件里。服务只关心自己的业务逻辑剩下的交给网关。1.2 为什么是 Spring Cloud Gateway 而不是 Zuul很多人会问这个问题。Zuul 1.x 基于 Servlet 同步阻塞模型每个请求占用一个线程网关这种高并发入口用同步模型很容易把线程池打满。Spring Cloud Gateway 基于 Spring WebFlux 和 Reactor是异步非阻塞模型底层 Netty 处理请求同样的硬件条件下能扛住的并发要高一截。这不是理论上的差别。我做过一次简单压测同样的路由转发配置Zuul 1.x 在 200 并发下线程池就已经出现排队Spring Cloud Gateway 到 500 并发还能保持比较低的延迟。虽然 Zuul 2 也改成了异步模型但 Spring Cloud Gateway 作为 Spring Cloud 官方主推组件和 Spring Boot 生态的兼容性、社区活跃度、文档完善度都更有优势。还有一个很现实的原因Spring Cloud Gateway 和 Spring Cloud 其他组件的集成太顺了。Nacos 做服务发现、Sentinel 或 Resilience4j 做熔断、Spring Cloud Sleuth 做链路追踪全是现成的配合不用自己拼轮子。2. 路由转发Predicate 和 Filter 的正确打开方式2.1 路由的三个核心概念Spring Cloud Gateway 的路由由三部分组成 Route路由、Predicate断言、Filter过滤器。Route 是一条路由规则它决定什么样的请求走哪个下游服务。Predicate 是匹配条件满足条件就匹配这条路由。Filter 是请求在转发前后执行的逻辑。理解它们的关系用生活类比就是路由器就像小区门卫Predicate 是他手里的访客名单他对你说你名字在名单上可以进Filter 就是进去之前的安检查你包里有没有违禁品。先说 Route 的配置。最小可用的路由长这样spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - StripPrefix2id 是路由名字自己定义日志和监控里会用到。uri 是转发目标lb://user-service表示通过负载均衡器去找注册中心里的 user-service 服务实例这里的 lb 是 load balancer 的缩写是 Spring Cloud Gateway 内置的负载均衡协议。Predicates 里的Path/api/user/**表示路径匹配规则所有以/api/user/开头的请求都会匹配到这条路由。Filters 里的StripPrefix2表示转发时去掉前两段路径也就是把/api/user剥掉下游服务收到的是/xxx这样的路径。2.2 path/api 前缀转发的各种玩法我见过很多团队在路径设计上有不同习惯总结下来大概三种第一种前端调用路径和下游服务路径完全隔离。前端访问/api/user/info网关剥掉/api/user后转发为/info。优点下游服务不用关心网关的存在路径怎么改都不影响前端。缺点是前端路径和下游路径对不上排查问题时要多做一次路径转换的脑内运算。第二种前端路径直接映射下游路径不做前缀剥离。/api/user/info直接转发成/api/user/info。这种配置简单但要求每个服务的 Controller 路径都带上自己的服务名前缀防止不同服务之间的路径冲突。第三种混合模式。一部分路由用前缀剥离一部分直接透传。比如给第三方开放接口的服务直接透传内部管理端用前缀剥离。这种方式最灵活但对运维要求高每个路由都要仔细确认转发规则。实战中我推荐团队统一用第一种。路径从网关到下游做一次映射服务内部路径怎么设计都自由而且你可以在网关层通过重写路径做版本管理。比如同一个服务接口V1 版本路径是/api/user/v1/infoV2 版本是/api/user/v2/info网关根据请求头或其他条件改写路径下游服务完全不用动。2.3 路由优先级与断言失效的坑路由匹配是先到先得的Spring Cloud Gateway 按声明顺序逐个匹配路由第一个能匹配的生效。这意味着路由声明顺序很关键。我踩过一个很经典的坑。用户服务和管理后台服务有重叠路径用户服务的路由声明在前管理后台的请求全部被转发到用户服务了后台页面各种 404。排查了半天才发现是路由顺序问题——管理后台的/api/admin/**和用户服务的/api/user/**本身不冲突但因为两者都配置了/api/**这样的兜底路由先声明的那条把请求全吃了。解决方法是把具体路径的路由放在前面兜底路由放在最后。规范做法是精确路径优先模糊路径靠后。我一般要求团队在配置里把路由按具体程度从高到低排序并且在每条路由上加注释说明用途。另一个容易踩的坑是谓词After、Before、Between的时间格式。这三个谓词用于时间段控制时间格式是带时区的 ZonedDateTime很多人直接写2025-01-01 00:00:00就启动报错正确写法是predicates: - After2025-01-01T00:00:0008:00[Asia/Shanghai]3. 统一认证一个全局 Filter 解决所有服务的鉴权难题3.1 认证方案选型Token 放哪里网关层做统一认证首先要想清楚认证的粒度。认证Authentication是确认你是谁鉴权Authorization是确认你能干什么。我建议网关只做认证把鉴权留给下游服务或者再加一层权限服务。原因很简单网关是所有流量的汇聚点如果它在每个请求上都做细粒度的权限判断性能压力会很大而且权限规则经常变每次都改网关并重启不是一个好方案。网关负责验证 Token 合法性把用户 ID、角色等基础信息透传给下游下游服务自己判断这个用户能不能执行某个操作。Token 怎么传也是选型的一部分。常见两种JWT 无状态方案和 Redis 存储方案。JWT 的优点是服务端不保存会话网关验签通过就完事天然支持分布式。缺点是签发后难以主动失效用户改了密码旧 Token 还是有效的。Redis 方案的优点是想踢谁就踢谁缺点是每次请求网关都要查一次 Redis多一次网络开销。我在生产上推荐 JWT Redis 黑名单的结合方案。正常请求走 JWT 验签性能好无状态用户登出或改密码时把 Token 的 jtiJWT ID加入 Redis 黑名单黑名单设置过期时间和 Token 的剩余有效期保持一致。3.2 JWT 解析与白名单机制网关里统一认证的 Filter 核心逻辑是这样的请求进来先检查路径是否在白名单白名单直接放行比如登录接口、注册接口、验证码接口这些不需要认证就能访问的路径。不在白名单的从请求头里取 Token验签验签不过返回 401验签过了就把用户信息放入请求头转发给下游。白名单建议做成配置中心的动态配置不要硬编码在代码里。因为业务上经常会临时加白名单路径比如微信回调、支付宝回调这些第三方回调接口配置在 Nacos 或 Apollo 里改完不用重启网关立即生效。看一个简化的 JWT 认证过滤器核心逻辑Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Autowired private JwtTokenProvider tokenProvider; Value(${gateway.auth.skip-paths:}) private ListString skipPaths; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getPath().value(); // 白名单直接放行 if (skipPaths.stream().anyMatch(path::startsWith)) { return chain.filter(exchange); } // 从 Header 取出 Token String token request.getHeaders().getFirst(Authorization); if (StringUtils.isBlank(token) || !token.startsWith(Bearer )) { return unauthorized(exchange); } // 校验 Token try { Claims claims tokenProvider.parseToken(token.replace(Bearer , )); // 把用户信息放入请求头透传给下游服务 ServerHttpRequest mutatedRequest request.mutate() .header(X-User-Id, claims.get(userId).toString()) .header(X-User-Name, claims.get(userName) null ? : claims.get(userName).toString()) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } catch (Exception e) { return unauthorized(exchange); } } Override public int getOrder() { return -100; // 数值越小越先执行保证在转发前完成认证 } }这里有个很关键的设计点用户信息通过X-User-Id、X-User-Name这样的自定义 Header 透传给下游而不是把原始 Token 转出去让下游再解一遍。这样下游服务不需要依赖 JWT 的密钥也能拿到当前用户上下文而且如果以后从 JWT 换成 Session下游代码不用改。3.3 透传用户上下文的约定规范Header 透传这种方案用起来很顺手但有个必须注意的问题这些内部 Header 不能允许客户端伪造。用户如果自己构造请求塞一个X-User-Id: admin的 Header网关会把假用户信息透传给下游造成越权。解决方案是在网关层做覆盖。具体做法是网关在认证通过后用解析出的用户信息覆盖掉请求头里同名的 Header而不是只在没有该 Header 时才设置。看上面代码里用的是.header(X-User-Id, claims.get(userId).toString())这个方法在 Header 已存在时会覆盖原值。这样客户端伪造的内部 Header 会在网关层被替换成真实用户信息。我建议把这些内部 Header 的命名规范写入团队开发文档比如统一以X-开头写明哪些 Header 是网关卡控的下游服务读取时不做任何信任假设。同时在下游服务侧做一层防御对网关层传入的X-User-Id服务端也要保持警惕禁止直接用它做越权查询权限判断仍要基于服务自身的鉴权逻辑。3.4 登出和 Token 刷新时网关要做什么很多团队的网关只做了认证放行忽略了登出和 Token 刷新这两个场景。登出场景客户端调登出接口后Token 在过期前仍然是合法状态如果不做任何处理Token 被泄露出去还能继续用。处理方式是把登出的 Token 加入黑名单网关在验证 JWT 后、放行前先查一下 Redis 黑名单如果在黑名单直接 401。Token 刷新场景刷新接口给你一个新 Token但旧 Token 在剩余有效期内还能用。如果不想让旧 Token 继续生效需要在刷新时把旧 Token 加入黑名单。这里有一种特殊情况需要考虑如果用户在 A 设备刷新了 TokenB 设备还拿着旧 Token 在正常使用如果刷新就把旧 Token 拉黑B 设备就会被迫重新登录。所以是否在刷新时拉黑旧 Token取决于你的产品策略——是一个账号同时只能一个设备在线还是允许多设备在线。这些逻辑听起来不难但要和网关的认证 Filter 联动在代码层级做好黑名单查询的短路处理。建议在 Filter 链路里把黑名单校验放在 JWT 验签之后因为验签能快速排除掉格式非法的 Token减少不必要的 Redis 查询。4. 限流熔断保护下游服务的最后一道防线4.1 RequestRateLimiter 限流器的工作原理网关的限流器用的是 Spring Cloud Gateway 内置的RequestRateLimiter它底层走的是令牌桶算法。令牌桶的原理是系统以固定的速率往桶里放令牌每个请求进来要拿走一个令牌桶里没有令牌就拒绝或排队。这个算法的好处是能应对突发流量——桶里可以攒下一些令牌短时间的高峰能把攒的令牌用完整体又不至于被打垮。RequestRateLimiter 需要两个必须的参数redis-rate-limiter.replenishRate和redis-rate-limiter.burstCapacity。replenishRate 是令牌补充速率也就是每秒允许的请求数burstCapacity 是桶容量也就是短时间内允许的突发请求峰值。配置一个按路径限流的路由spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 50 redis-rate-limiter.burstCapacity: 100 key-resolver: #{userKeyResolver}这里的key-resolver指定了一个 Bean 名决定限流维度。Spring Cloud Gateway 默认提供PrincipalNameKeyResolver按登录用户名限流。如果你用默认配置未登录用户会被集中到一个桶里效果很差所以实际项目里几乎都要自定义。4.2 自定义 KeyResolver按用户、按 IP、按接口限流KeyResolver 的作用是这个请求应该算在哪个桶里。根据业务不同限流维度不同。接口维度限流每个接口路径是一个桶配置统一的限流阈值避免某个接口被刷爆拖垮整个服务。实现是返回请求路径。用户维度限流每个用户一个桶防止单个用户恶意刷接口。实现是取 Header 里的X-User-Id。IP 维度限流每个客户端 IP 一个桶适合匿名接口的防护。获取客户端真实 IP 有讲究因为网关前面通常还有 Nginx 或 SLB要从X-Forwarded-For头里取第一个 IP不能直接用request.getRemoteAddress()那个拿到的是上一跳代理的地址。实际的 KeyResolver 代码Bean public KeyResolver userKeyResolver() { return exchange - { String userId exchange.getRequest().getHeaders().getFirst(X-User-Id); if (StringUtils.isBlank(userId)) { userId exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); } return Mono.just(userId); }; }这个实现是先取用户 ID取不到就退回 IP保证所有请求都能分到对应的桶。这里要特别注意KeyResolver 返回的 key 如果为空或都是同一个值会导致所有请求进同一个桶限流阈值瞬间被打满线上会出现大面积误拦。我遇到过因为网关没有正确透传用户 ID导致所有请求的 KeyResolver 拿到同一个默认值结果 1000 个用户共享一个 100 容量的桶高峰期大量误拦截。排查时看限流日志里 key 的分布就能发现问题。4.3 基于 Resilience4j 的熔断降级限流解决的是入口流量过大的问题熔断解决的是下游服务已经挂了别再把请求打过去的问题。网关的熔断我推荐用 Resilience4j CircuitBreaker它是 Spring Cloud Gateway 官方推荐的熔断实现比 Hystrix 轻量不少Hystrix 已经进入维护模式了。熔断器的状态机有三个状态关闭Closed、打开Open、半开Half-Open。关闭状态正常放行所有请求但会统计失败率失败率超过阈值就变成打开状态打开状态下直接拒绝请求不会再往后端打过一段时间后进入半开状态放少量试探请求如果试探成功就恢复关闭失败则回到打开。网关集成 Resilience4j 的配置spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - name: CircuitBreaker args: name: userServiceCB fallbackUri: forward:/fallback/user resilience4j: circuitbreaker: configs: default: slidingWindowSize: 20 failureRateThreshold: 50 waitDurationInOpenState: 10s minimumNumberOfCalls: 10参数解释slidingWindowSize是滑动窗口大小记录最近 20 次请求failureRateThreshold是失败率阈值50 表示失败率超过 50% 就打开熔断器waitDurationInOpenState是打开状态持续时间10 秒后进入半开状态minimumNumberOfCalls是触发熔断判断的最小请求数避免请求太少时因为偶然失败就熔断比如刚上线只来了 5 个请求失败了 3 个就熔断那不合理至少要 10 个请求才做统计。4.4 熔断降级的参数调优心得参数配置是最见功夫的地方。很多团队默认配置一把梭生产上出问题才回头调。我的经验是failureRateThreshold不要一开始就定死要看你下游服务正常的错误率基线。如果下游服务本身错误率就在 10% 左右你把阈值定在 10%那每隔几分钟就熔断一次业务全被拦了。建议先定一个不伤业务的宽阈值比如 50%然后观察一周根据实际错误率再收紧。waitDurationInOpenState也要结合下游服务的恢复时间定。下游服务如果是瞬时抖动几秒就能恢复10 秒没问题如果是数据库连接池被打满、需要手动重启的应用10 秒肯定不够建议 30 秒起步。熔断的 fallbackUri 是兜底返回。我一般给网关配一个统一的兜底 Controller返回一个结构化的错误 JSON而不是裸的 500 页面。前端拿到这个 JSON 可以统一提示服务繁忙请稍后再试。这里有一个容易忽略的点fallback Controller 是在网关应用内部处理的不会再经过你自定义的 GlobalFilter 和路由过滤器所以不要在 fallback Controller 里依赖用户上下文之类的透传信息它拿不到。要做兜底就把兜底逻辑写得完全自包含。5. 企业级落地配置管理、链路追踪与性能调优5.1 网关配置的动态刷新网关的路由、限流、熔断参数这些都建议放在配置中心管理。我用的是 NacosSpring Cloud Gateway 提供RefreshRoutesEvent事件配置变更后手动或自动发布这个事件路由就能热刷新不用重启网关。实际做法把配置放在 Nacos 的共享配置里监听配置变更事件变更后调用RefreshRoutesEvent。核心代码Component public class GatewayRefreshListener { Autowired private ApplicationEventPublisher eventPublisher; EventListener public void onNacosConfig(NacosConfigChangeEvent event) { eventPublisher.publishEvent(new RefreshRoutesEvent(this)); } }在 Nacos 配置里的 text 格式 JSON 需要和网关 yml 里的spring.cloud.gateway前缀对齐spring.cloud.gateway.routes[n].id这种结构。有一个细节用 Nacos 管理路由后本地 application.yml 里还留着旧路由两边会合并吗不会的如果你设置了spring.cloud.gateway.discovery.locator.enabledtrue或者从远端拉取路由本地和远程的配置是叠加的很容易出现路由重复导致转发错乱。我建议团队约定路由配置要么全部本地要么全部远端不要混着写。5.2 链路追踪从入口看到全链路网关是所有流量的入口做链路追踪价值最大。网关接入 Spring Cloud Sleuth Zipkin每进来一个请求就在网关生成一个 traceId之后请求转发到下游服务traceId 一直传递下去这样排查慢请求时就能看到完整链路——在哪一个服务上慢的、慢了多少毫秒。网关传递 traceId 是自动的Sleuth 会通过 B3 Propagation 的 HeaderX-B3-TraceId、X-B3-SpanId把上下文传给下游。需要注意的一点是如果你在网关里用了自定义 GlobalFilter并且这个 Filter 里自己发 HTTP 请求要确保用的是 Sleuth 包装过的 RestTemplate 或 WebClient否则你自己发出去的子调用的 traceId 会断层排查问题时就断链了。5.3 性能参数调优线程池、缓冲区、超时Spring Cloud Gateway 基于 Netty默认参数对大部分业务够用但生产环境还是有几个参数值得调。一是 HTTP Client 的响应超时。网关转发请求给下游如果下游一直不返回网关会一直占着连接。默认超时是 5 秒如果你下游有大查询的接口可能要 30 秒以上不加长超时就会误报失败。配置方法spring: cloud: gateway: httpclient: connect-timeout: 1000 response-timeout: 10sconnect-timeout 是连接超时建议 1 秒以内因为服务发现模式下连接同一个注册中心的服务通常都是内网握手不该超过 1 秒。response-timeout 建议比下游服务的 P99 延迟略大比如下游 P99 是 8 秒响应超时就给 12 秒。二是 Netty 的线程数。默认情况下网关用机器核心数的线程数跑如果你的网关部署在 2C4G 的小机器上Netty 只有 2 个 worker 线程压测的时候会发现吞吐上不去。这时候要评估有没有必要提高线程数但说实话我一般不建议普通团队去动这个参数Netty 的线程模型调错了反而会引发更多问题。三是缓冲区参数。如果你的业务有上传文件的场景网关默认的请求体大小限制在 10MB 左右大文件上传要在全局配置里加大spring: codec: max-in-memory-size: 20MB这个参数容易被忽略一旦上传大文件报 413很多人都以为是 Nginx 的问题其实网关这层也有一道关卡。我这里特别提醒一点spring.codec.max-in-memory-size设置的是单条消息的内存上限给大了会影响网关的内存占用给文件上传做单独的转发路由更合理不要无脑全局加大。5.4 网关部署的核心注意点网关是无状态的可以多实例部署前端通过 SLB 或 Nginx 做负载均衡。多实例部署要特别注意限流的维度RequestRateLimiter 依赖 Redis 做计数多实例共享一个 Redis 时限流统计是全局的效果正确。但如果你的 Redis 是单点要考虑高可用方案否则 Redis 挂了网关的限流功能就废了。熔断的状态也是存在各实例内存里的多实例部署时每个实例独立判断这是正常的。问题是一旦某个下游服务出问题多个网关实例会各自独立触发熔断下游服务收到的探测请求数量是实例数的倍数如果计划用半开状态做恢复探测注意不要配置得太激进否则恢复期的探测流量本身就能把刚缓过来的服务再次打垮。网关机器建议和应用服务分开部署不要混部。网关的 CPU、内存、网络占用都有自己的特征混部会让监控指标变得很难分析——响应变慢了说不清是网关的锅还是被同机应用抢了资源。6. 生产环境踩坑笔记与个人体会6.1 我只想说的几个真问题第一个是 Spring Cloud Gateway 的版本兼容问题。Spring Cloud 和 Spring Boot 的版本是强对应的两者的版本号必须匹配。最典型的就是 Spring Cloud 2020.0.x 要求 Spring Boot 2.4.x 及以上你用的 Spring Boot 是 2.3.x启动可能报各种 ClassNotFound 或者配置不生效。我建议不要自己拍脑袋配版本直接用 Spring Initializr 生成的版本组合或者用 Spring Cloud 官方版本说明文档对好版本再动手。第二个是网关的日志要单独治理。网关流量大日志量也大。全打 INFO 的话一天几 GB 日志很正常磁盘扛不住。我建议网关只打 WARN 和 ERROR以及路由转发的关键摘要来源 IP、路径、目标服务、耗时、状态码。把摘要日志打成 JSON 格式接入 ELK 或 Loki排查问题效率高很多。第三个是网关超时和下游超时的关系要理清。网关的超时时间和下游服务的超时时间是两个独立的配置网关的响应超时一定要大于下游服务的处理超时否则下游还在处理网关先行超时前端收到 504但下游逻辑可能已经执行了。尤其是在下单、支付这类关键流程上这种假失败比真失败还坑因为用户看到的是失败业务却可能已经扣了款。6.2 网关别做成上帝服务最后说点个人体会。网关能做的事非常多但并不是所有事都适合扔到网关里。我见过有的团队把数据脱敏、业务校验、甚至部分业务逻辑都塞进了网关理由是这样就统一了。结果网关变成一个巨型应用每次改动都要重新发布发布失败整个系统入口瘫痪。我给自己定的原则是网关只做横切关注点cross-cutting concerns——路由转发、统一认证、限流、熔断、链路追踪、基础日志。凡是和具体业务场景强相关的逻辑都不放在网关里。判断标准很简单如果一个功能做了某个服务不需要这个功能那它就不该放网关。另一个体会是要重视网关的监控告警。网关是流量入口出问题影响的是所有服务。我一般会给网关单独配一套告警规则请求量突降、5xx 比例升高、平均延迟上升、限流触发次数异常这些都要第一时间告警。很多团队把注意力放在业务服务上网关的监控没人看等发现问题时往往已经对线上造成了比较明显的负面影响。回看这几个项目的落地过程Spring Cloud Gateway 本身的学习成本并不高真正的难点在于把认证、限流、熔断这些通用能力和你自己的业务场景结合起来选对参数、理清边界。希望这篇实战记录能帮你少走一些我已经走过的弯路。