公司的网关老出问题,改一个过滤器,常常连带把别人功能打飞,几次线上故障都能追到这。接下来把事儿从头往下捋一捋,别看表面都是“某人改了点代码”,根儿实则在团队里每个人都想把自己的需求做成一个小玩意儿——自定义过滤器。项目里慢慢堆出了太多相互重叠的逻辑,边界模糊,谁也说不清哪个过滤器该先跑、哪个该后跑,结果一个改动就把别人链条蹦开了。
先说背景。最开始网关很简单,几条路由,几个常用过滤器,满足最基本的转发、鉴权之类的需求。业务一多,团队也大了。新同事接入新服务,各自按自己的理解加拦截、改头、改参、签名校验、鉴权,甚至为了一点点小场景单独写了个过滤器。时间长了,网关里出现几十个自定义过滤器,功能上相互重复,谁也不敢随意删,维护成本就像雪球越滚越大。
问题爆发的排查过程也很常见。一次线上报警,说某个服务的认证失败。我们沿着日志倒回去,发现请求头被改写,token没了。继续追溯到一次合并记录,某个提交里新增了一个 SetRequestHeader,直接覆盖了原 header,没想到下游的另一个服务还靠这个 header 做鉴权。把这些事儿串起来之后,才意识到许多自定义逻辑实则能用框架自带的过滤器搞定,根本不需要每次都动手写代码。
说点可替代的内置能力,举例子方便大家落地用。Spring Cloud Gateway 本身自带许多过滤器,官方文档里写着三十多种,平时大家只会用几个常见的,像 RewritePath、StripPrefix,其他的能省事不少。
在请求头方面,有三把常用刀:
– AddRequestHeader:转发前给请求追加一个头,像加上 X-From: gateway,下游可以用来判断来源。
– RemoveRequestHeader:把客户端带来的敏感头删掉,典型场景是网关统一鉴权,下游不需要直接看到原始 token。
– SetRequestHeader:强制覆盖某个头,跟 Add 不同的是前者会把旧值替换掉。
参数层面也有对应工具:AddRequestParameter / RemoveRequestParameter 可以在 URL 或表单里加上灰度标识,或者把 password 之类的敏感字段去掉。
请求体处理上有门道:默认情况下请求体只能读一次。遇到要做签名校验然后再解析参数这种链式操作,就得先用 CacheRequestBody 把 body 缓存住,后续的过滤器才能反复读取。还有一些与大小有关的过滤器,像 RequestSize、RequestHeaderSize,可以在网关层直接把超大文件或奇形怪状的头给拒了,免得下游被拖垮。
关于 Host 和代理,有两项常常用:
– PreserveHostHeader:保留客户端的 Host,某些后端依赖原始 Host 时会用到。
– ModifyHostHeader:把 Host 写成某个固定值,反向代理场景常常需要。
响应层面也并非空白。AddResponseHeader、RemoveResponseHeader、SetResponseHeader 能在服务返回后对 header 做调整,列如统一加缓存策略,或者删掉会暴露内部信息的头。需要按正则改写 header 时可以用 ModifyResponseHeader。有时候响应里出现重复的同名头导致跨域或浏览器问题,
RemoveDuplicateResponseHeader 可以去重。还有个比较实用但要看版本支持的,是把 JSON 返回体里某些字段删掉的过滤器(
RemoveResponseBodyJsonField),当下游短期内不能改接口但又不想暴露某些字段时挺有用。
路径处理方面的选择也多:RewritePath 用正则替换路径,灵活;PrefixPath / AddPrefix 给路径加前缀,StripPrefix 按段数删掉前缀,这些和 RewritePath 的按正则替换有本质区别,别混着用出问题。需要重定向可以用 RedirectTo,想直接把路径设置成模板就用 SetPath。反向代理中还会遇到 Location 跳转要改写的场景,RewriteLocation 可以改响应头里的 Location。
流量管控与容错也不是只能靠外面那层自己实现。RequestRateLimiter 是基于令牌桶的限流器,一般把计数放在 Redis,KeyResolver 决定以什么维度限流(IP、用户 ID 等)。这个限流器配置得不对很容易把正常流量误伤,尤其是 key-resolver 不靠谱时。Retry 能在特定状态或超时情况下自动重试,但要超级注意幂等性,别让重试把业务操作给重复了。老的 Hystrix 在不少项目里被弃用,目前多数人改用基于 Resilience4j 的 CircuitBreaker,可以配置失败率、超时阈值和降级策略。还有 SaveSession,能在转发前把 WebSession 写出去,配合 Spring Session 的懒写机制,避免下游读不到 session。
举点实际配置样式,说明怎么写:
– 要在请求上加来源头:在路由里配一个 AddRequestHeader,键名是 X-From,值写 gateway。
– 要覆盖请求头:用 SetRequestHeader token ${some-template} 之类的模版写法。
– 要把 POST body 缓存:用 CacheRequestBody,然后后续从 exchange.getAttribute 里取。
– 限流场景:启用 RequestRateLimiter,配好 Redis,别忘了写一个靠谱的 key-resolver(按 IP 或按用户 ID)。
回到我们团队的实践。把重复实现慢慢替换成内置配置后,冲突少了不少。还有一点特重大:过滤器顺序。路由上的过滤器先后决定了 header 最终值、请求体是否已缓存等关键状态,顺序一错,问题就会很奇怪、很难排查。于是我们约定了一个模板:把常规的内置过滤器写在配置里,复杂的业务逻辑才上自定义过滤器。对限流特别强调,使用 RequestRateLimiter 时必定要配好 Redis 和一个可靠的 key-resolver,否则规则就会跑偏,给线上带来麻烦。
还有些细节也常被忽略。列如有人以为 AddRequestHeader 和 SetRequestHeader 是同一类事儿,结果多个服务都追加了同一个头,导致下游看到重复 header 行为异常。或者在做签名校验时没缓存 body,导致后续解析失败。还有自定义过滤器里直接操作 exchange 的属性,但没思考并发,导致数据串线。这些坑都是常见的,不是某个人的错,而是整个开发习惯和约定造成的。
技术上有些小技巧能少走弯路:先把路由和常用内置过滤器列成模版,供新同事引用;把常见的 key-resolver 实现放到公共库里,别每个服务再重复写一遍;对于必须自定义的过滤器,写清楚边界和副作用,并在合并时专门交底。这样大家工作的时候就像在施工图上按格子填色,不再随手画乱线。
最终,许多线上故障都能从“哪个过滤器先干了什么”这件事上找到缘由。像那次由于 SetRequestHeader 覆盖 token 导致认证失败,或者由于没有 CacheRequestBody 导致签名校验读不到 body,这些都是能被复盘和规避的。要是团队一开始就约定好优先使用内置能力,必要时才写自定义过滤器,许多麻烦都不会发生。最后一句实用提示:限流要上 Redis,key-resolver 要靠谱,过滤器顺序别随意改。

收藏了,感谢分享