Spring Cloud Gateway过滤器详解:GatewayFilter与GlobalFilter实战与避坑指南

发布时间:2026/10/13 5:29:52
Spring Cloud Gateway过滤器详解:GatewayFilter与GlobalFilter实战与避坑指南 2020年3月31日我正在做一套微服务改造路由转发的问题刚解决完就发现网关真正难啃的部分是过滤器。当时留了一份标题为《3-服务网关Gateway_网关过滤器学习笔记》的记录今天把里面关于过滤器的内容重新整理了一遍结合后来在几个项目里反复验证过的经验一次性讲透。这篇文章围绕Spring Cloud Gateway中的过滤器机制展开重点包括GatewayFilter和GlobalFilter怎么区分、内置过滤器怎么用、自定义过滤器怎么落地以及过滤器链上那些坑是怎么踩的。想弄清楚鉴权、日志、限流、请求改写这类横切逻辑到底该放在哪一层、怎么写才稳的可以直接参考。我先说结论Route只是把请求带到目的地真正让网关产生业务价值的是过滤器链。1. 路由只是入口过滤器链才是网关真正的执行引擎1.1 从Route的结构里看Filter的位置Spring Cloud Gateway把一条路由Route拆成了很清晰的几部分id、uri、predicates、filters。predicates决定一个请求该不该进这条路由filters决定这个请求在转发前后要被做哪些处理。从配置层面看filters就是一组规则的列表但到了运行期框架会把这一组规则解析成具体的GatewayFilter实例再和另一批全局过滤器合并组装成一条完整的GatewayFilterChain。我在学习早期有个认知偏差以为路由匹配完就直接转发下游了。实际上请求从进网关到转发出去要穿过一整串过滤器每个过滤器都可以对这次请求做修改、阻断或者记录。甚至连真正的HTTP转发动作本身在框架里也是一个过滤器——只不过它排在整个链条的末端。理解了这一点就明白为什么说网关的扩展能力几乎全部集中在过滤器上。1.2 请求在过滤器链上的生命周期如果用一句话描述过滤器链的执行流程可以这么说请求沿链逐个进入每个过滤器在转发之前做前置逻辑转发动作发生在链条末端响应返回后再沿链反向做后置逻辑。这里有个用同步Web思维理解容易出错的地方Gateway底层是Spring WebFlux的响应式模型每个过滤器返回的是Mono 执行顺序不是普通的for循环而是通过Reactor的流式调用把过滤器串起来的。所以写过滤器时不能在想做后置处理的地方直接写一段同步代码而要把它放在chain.filter(exchange)返回的Mono上做回调比如用.then(...)或.doFinally(...)。我做过一个最简单的实验来验证这个模型写一个全局过滤器在进入时打印“pre”在then回调里打印“post”在路由转发过滤器里再打印“route”。最终日志顺序是pre、route、post而不是pre、post、route。这就是后置逻辑是转发完成之后以回调方式触发的最直观证据。1.3 为什么要把横切逻辑放在网关这一层在做微服务改造之前我的很多项目都是把鉴权、日志、透传用户信息这几件事写在每个服务各自的代码里。服务一多问题就来了一部分服务把token校验写在了拦截器另一部分写在AOP里还有的直接在Controller里手写重复代码多强度还不一致。把这类横切逻辑收敛到网关过滤器之后下游服务不用再关心这些通用规则只需要信任网关已经处理好的请求。这带来的直接收益是改造某个服务时通用逻辑不用跟着改安全规则有调整只改网关一处即可。当然这里也有一个度的问题后面我会单独说哪些逻辑适合放网关、哪些不适合不然网关会变成一个什么都干的庞然大物。2. GatewayFilter和GlobalFilter作用范围差很多2.1 两类过滤器的职责边界刚接触过滤器时很自然的困惑是既然都叫过滤器为什么还分GatewayFilter和GlobalFilter两种我当时也花了一段时间才理清。GatewayFilter是路由级的它必须被某条路由显式引用才会生效。内置的那些带名字的过滤器比如StripPrefix、AddRequestHeader、Retry本质都是GatewayFilter。而GlobalFilter是全局过滤器只要定义成Spring容器中的一个Bean它就会自动作用到所有路由上不需要在路由配置里手动挂载。可以用一个不太严谨但很好理解的类比GatewayFilter是这条路由的专属保安只负责这条路上的请求GlobalFilter是整个园区大门的保安任何人进来都要过一遍。维度GatewayFilterGlobalFilter生效范围绑定在某条路由上全局所有路由定义方式路由配置filters引用或RouteLocator设置定义Bean并实现GlobalFilter、Ordered常见用途路径改写、去前缀、加响应头、鉴权白名单内全局日志、TraceId、统一认证、限流配置密度随路由数量增加而增加全局一份不随路由变化2.2 合并排序GlobalFilter并不一定先执行这是我在很长一段时间里的错误认知以为框架会先执行GlobalFilter再执行路由上的GatewayFilter。翻源码之后才知道最终进入过滤链的是两者合并后按Order统一排序的结果。具体来说所有参与当前请求的过滤器都会按照getOrder()方法的返回值从小到大排列数值小的一侧先执行。内置的路由转发过滤器NettyRoutingFilter排在链路最末端order数值极大所以请求会先经过所有业务过滤器最后才真正发往下游。如果你自定义了一个order为0的GlobalFilter而某条路由的StripPrefix过滤器order是默认的0它们的执行顺序是不确定的甚至与配置顺序有关这一点容易埋坑。所以设计全局过滤器时订单号最好留出明显的缓冲区间。比如我习惯把日志Trace过滤器放在-1000把认证过滤器放在-100在中间给一般过滤器留空间。不建议把很多过滤器都设在同一个order值因为可能引发不可控的执行先后问题。2.3 pre阶段和post阶段的顺序细节很多资料说过滤器链按照order从小到大执行这句话只对了一半。准确的描述是在转发前的pre阶段请求按order从小到大的顺序逐个进入过滤器但在转发完成后的post阶段触发顺序是反过来的后进入的过滤器反而先执行后置逻辑。这本质上是因为后置逻辑是回调回调会沿着调用栈从内往外触发。我当时写了一个日志过滤器以为在post阶段往响应头里加耗时不会有问题结果发现如果这个日志过滤器order很小而后面还有别的过滤器改了响应那么它的post反而发生在更后面的修改之前。这也是为什么我建议如果你要记录完整耗时过滤器要尽量靠前如果你要确保自己的响应头是最终值反而要让自己的order更大让它在post阶段更早回到响应链路。这种细节只有实际调过过滤器的人才会注意到。3. 内置过滤器先记住这些日常开发足够用3.1 路径类过滤器StripPrefix、PrefixPath、RewritePath路径改写在网关层非常高频。最常见的是前端通过网关访问后端服务时路径里带了一层接口前缀但下游服务并不想要这个前缀。比如路由Path是/api/order/**下游服务的接口是/order/**这时用StripPrefix1就能把/api/order/1转成/order/1spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1注意StripPrefix的参数是剥离的段数。StripPrefix2会把/api/order/1转成/1我在一开始没细想把2当成保留后两段调了半天才发现理解反了。如果要做更灵活的路径转换比如把/api/order/1转成/order/detail/1这种不完全规则化改写就要用RewritePath它基于正则表达式filters: - RewritePath/api/(?segment.*), /$\{segment}这里有一个非常容易踩的YAML坑$符号在Spring Boot的配置里会被当成占位符前缀必须写成$\{...}否则启动时解析会出问题。我自己第一次配置时直接写了/${segment}结果运行时报参数解析错误查了半天才发现是转义问题。3.2 Header类过滤器加头、去头、改头Header处理是网关层面最常规的操作内置过滤器里有一整套Add/Remove/Set开头的工具。比如下游服务需要知道请求来自哪个网关版本可以加一个请求头X-Gateway-Versionfilters: - AddRequestHeaderX-Gateway-Version, 1.0对外部用户传入的请求头做清理也很常见。有一次我在项目中遇到一个问题用户通过请求头X-Forwarded-For自己传了个内网IP后端在打印日志时被误导了很久。最后就是在网关加了一个RemoveRequestHeader在转发前把不可信的头去掉统一由网关写入可信值。响应头也可以用过滤器加比如统一给响应加一个X-Response-Time头记录网关处理耗时。不过如果你有多个过滤器都会操作响应头要注意2.3节说的执行顺序问题。3.3 重试、限流、熔断三个会直接改变下游压力的过滤器这三个过滤器在真实项目里属于“用之前必须想清楚后果”的类型。Retry过滤器在GET请求上通常安全但用在POST、PUT这类写操作上要非常小心。如果下游已经写库成功了只是响应超时重试就可能导致重复写库。而且Retry如果要对请求体做重放必须保证能读取到body数据一旦前面的过滤器已经消费过请求体重试时再去读就没有数据了。这里面涉及请求体缓存细节比较多我在第6节会专门讲。RequestRateLimiter是配合KeyResolver限流用的它的参数里需要指定RedisRateLimiter相关配置。这里我提一个经验限流Key不要只拿IP来做区分度太低很多情况下加一层用户ID或者接口维度的组合更好。默认的PrincipalNameKeyResolver在很多场景下并不够用建议自定义KeyResolver。熔断过滤器的名字要特别注意版本差异。2020年我用的时候配置名还是Hystrix后来换成了CircuitBreaker参数风格类似filters: - name: CircuitBreaker args: name: orderCb fallbackUri: forward:/fallbackfallbackUri有个硬性要求必须是forward:开头的本地转发不能写成外部的http地址。如果需要降级到另一个远程服务得先把降级逻辑做成本地Controller再在内部转发。3.4 内置过滤器怎么查命名规则比死记更高效内置过滤器数量很多我从不建议死记。这里有一个判断规则框架内置的GatewayFilterFactory命名非常规范比如AddRequestHeader对应AddRequestHeaderGatewayFilterFactory去掉GatewayFilterFactory后缀就是配置名。自定义过滤器也遵循这个规则所以只要记类别就够了。遇到不确定的配置项时用这个命名规则去查源码里的Factory类比看别人二手总结准确得多毕竟不同小版本的默认参数确实有变动。4. 自定义一个认证过滤器从Factory到路由挂载的完整路径4.1 为什么选择Factory模式而不是直接new一个过滤器自定义过滤器常见有两条路直接写一个类实现GatewayFilter接口或者继承AbstractGatewayFilterFactory再返回过滤器实例。我在早期喜欢直接实现GatewayFilter写起来直观但用了几次就发现弊端没法在路由配置里灵活带参数也没法通过配置文件控制开关。后来切换到Factory模式虽然类多了一层但把过滤器从写死的逻辑变成了可配置的组件这样就能在YAML里像用内置过滤器一样去引用它还可以传白名单开关、放行的路径前缀、Token字段名等配置项。这个模式更适合需要维护的项目。4.2 一个最小可用的认证过滤器下面是一个比较朴素的Auth过滤器它的核心职责就是三件事没有Token直接返回401有Token但校验不通过返回401校验通过后把用户信息通过Header透传给下游。public class AuthGatewayFilter implements GatewayFilter, Ordered { private final AuthConfig config; public AuthGatewayFilter(AuthConfig config) { this.config config; } Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String token exchange.getRequest().getHeaders().getFirst(Authorization); if (token null || !token.startsWith(Bearer )) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 这里省略真正的token解析和校验逻辑 String userId parseUserIdFromToken(token); if (userId null) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } ServerHttpRequest request exchange.getRequest().mutate() .header(X-User-Id, userId) .build(); return chain.filter(exchange.mutate().request(request).build()); } Override public int getOrder() { return -100; } }注意我在认证失败时用的是return exchange.getResponse().setComplete()这个返回值很重要。有些初学者只调用setComplete但没return结果逻辑继续往下面走最后还是执行了chain.filter导致故障请求照样被转发到下游。这个坑我在第6节再展开。4.3 把过滤器变成可配置组件Factory类写法继承AbstractGatewayFilterFactoryT泛型就是配置类。命名是关键如果类名是AuthGatewayFilterFactory在YAML里的配置名就是Auth框架靠这个命名规则做自动映射。Component public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactoryAuthGatewayFilterFactory.Config { public AuthGatewayFilterFactory() { super(Config.class); } Override public GatewayFilter apply(Config config) { return new AuthGatewayFilter(config); } public static class Config { private boolean enabled true; public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled enabled; } } }如果在同一个包里有多个自定义Factory注意所有类名后缀必须是GatewayFilterFactory不能有重名否则装配阶段会报错。4.4 在路由上挂载并测试配置里启用这个过滤器很简单spring: cloud: gateway: routes: - id: order-route uri: lb://order-service predicates: - Path/api/order/** filters: - Authtrue - StripPrefix1这里也体现了一个顺序细节配置中过滤器出现的顺序就是它们的执行顺序。如果先写Auth后写StripPrefixAuth会先执行反之路径已经被改写后再执行Auth也一样。通常把鉴权类的过滤器放在路径改写之前这样鉴权逻辑看到的是原始请求路径对白名单判断更直接。4.5 自定义FilterFactory的测试要点自定义过滤器不能只靠启动看日志。我习惯直接用真实网关实例配合Mock下游来验证重点是下面几种情况无Token请求是否返回401Token合法时下游是否拿到X-User-Id配置里enabledfalse时过滤器是否完全不生效。后者的验证容易被忽略如果配置开关都没生效说明Config的绑定大概率有问题需要检查setter方法和泛型配置类是否匹配。5. 全局过滤器实战日志Trace、用户信息透传、统一响应头5.1 用GlobalFilter打一份全链路日志全局过滤器是我做日志Trace最顺手的位置。一个全局日志过滤器只要做好三件事生成全局唯一的TraceId把它放进请求头在post阶段记录耗时。Component public class TraceGlobalFilter implements GlobalFilter, Ordered { private static final String TRACE_ID X-Trace-Id; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { long start System.currentTimeMillis(); String traceId UUID.randomUUID().toString().replace(-, ); ServerHttpRequest request exchange.getRequest().mutate() .header(TRACE_ID, traceId) .build(); log.info([gateway] request start, uri{}, method{}, request.getURI(), request.getMethod()); return chain.filter(exchange.mutate().request(request).build()) .then(Mono.fromRunnable(() - { long cost System.currentTimeMillis() - start; log.info([gateway] request end, uri{}, cost{}ms, request.getURI(), cost); exchange.getResponse().getHeaders().add(X-Trace-Id, traceId); })); } Override public int getOrder() { return -1000; } }有几个细节值得说明一是order尽量小让TraceId在进入链路最开始就被注入这样后面所有过滤器都能读取到二是post阶段写在then回调里而不是filter方法末尾因为filter方法返回的Mono表示整个过滤链完成之后才能触发后置逻辑三是响应头要在post阶段加否则等转发完成后这个过滤器已经没机会执行了。5.2 用户信息透传的防伪处理网关完成认证后下游服务怎么知道当前用户是谁主流方案是把用户信息放进Header透传下去。但这里有一个安全细节不能直接信任客户端传过来的X-User-Id。我在一个项目里就遇到过头被跳过的情况调用方在请求里伪造了X-User-Id下游服务直接拿这个头当登录用户用权限控制形同虚设。正确的做法是在网关认证通过后先remove掉原始请求里的X-User-Id再写入从Token解析出的可信userId这样下游拿到的只能是网关写入的值。ServerHttpRequest request exchange.getRequest().mutate() .headers(httpHeaders - { httpHeaders.remove(X-User-Id); httpHeaders.set(X-User-Id, userId); }) .build();通过mutate创建新请求再exchange.mutate().request(request).build()生成新的exchange是全局过滤器里最常见的操作模式。这里要特别强调ServerWebExchange本身是不可变对象你直接调用getRequest().getHeaders().set()是改不进去的必须用mutate返回新实例。这个点我见过很多人踩坑。5.3 全局过滤器里改请求体的正确姿势有时候不得不做响应式场景里的请求体重写比如把前端加密的报文解密后再转发。但全局过滤器里的exchange.getRequest()返回的是只读请求body流只能读一次改写body不能直接操作原始对象。正确姿势是用装饰器模式实现ServerHttpRequestDecorator在重写getBody()时返回一个新的FluxDataBuffer然后通过exchange.mutate().request(decrator).build()替换请求。但这里非常麻烦因为Content-Length、Content-Type要一起处理否则下游解析会出错。我在实际项目里对网关改body的方案比较保守除非有强需求否则宁愿让下游服务自己处理解密网关只做基础的转发和鉴权。理由很简单请求体重写属于容易出问题且排错成本高的逻辑放在网关收益有限还会拖慢转发性能。如果你真的要做至少要整理一套贴好测试样例的公共库而不是在路由里东写一个西写一个。5.4 统一响应头的两个误区全局过滤器统一加响应头看上去简单实际操作有坑。第一不要只加exchange.getResponse().getHeaders()——如果链后面还有过滤器修改了响应或者转发阶段设置了别的头顺序不对的话你的头会被覆盖或者出现在错误位置。第二响应头是HTTP协议层面的内容别把大体积业务数据塞进去会在后续排查时非常痛苦。我之前在一个项目里为了调试方便临时往响应头里加了完整请求体结果压测发现吞吐量明显下降才意识到自己的调试代码把响应撑大了。这类调试代码上线前要清干净。6. 过滤器链上那些让我掉过头发的坑6.1 认证失败后的短路返回setComplete之后必须return这是所有过滤器坑里概率最高的一个。典型错误版本是这样的exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); exchange.getResponse().setComplete(); // 少了return继续往下走 return chain.filter(exchange);执行后表现非常诡异客户端确实收到了401但日志里能看到请求还是转发到了下游服务。原因是setComplete()只是把响应标记为完成如果没有中断过滤链后面的过滤器依旧会执行甚至转发过滤器也会照常发起下游调用。正确写法是return exchange.getResponse().setComplete();这一个return就是短路逻辑的关键。很多限流、熔断的短路逻辑也都是一样的写法不要只看返回值类型是MonoVoid就觉得可以随意返回anything。6.2 RewritePath配置里的$转义坑了一整天的排错前面第3节提过#/的问题这里再单独拎出来强调一次在YAML配置RewritePath的正则替换表达式时$符号必须写成$\{segment}这种形式。我第一次遇到的报错是启动时报占位符解析错误。当时第一反应是正则写错了反复改了正则表达式结果完全没用。后来才想到Spring Boot会把${...}当作属性占位符去解析而YAML里的反斜杠又让这个符号变得非常隐蔽。如果你也遇到启动或路由解析异常先检查有没有这类转义问题比埋头改正则高效得多。6.3 order设置得太随意导致过滤器链执行顺序失控自定义GlobalFilter时如果order都设成默认值0执行顺序相对确定性就比较差。之前有个项目里很多人写的全局过滤器都是0相互之前谁先执行完全靠运气日志顺序每次重启都可能变化排查问题极其痛苦。我后来定了一条团队约定日志Trace过滤器设为-1000认证过滤器设为-100业务改造类过滤器统一在100以后紧急短路的过滤器用Integer.MIN_VALUE附近。这样排序的空间错开了也能够直观地在代码里看出过滤器的优先级。另一个注意事项是order最小值附近要谨慎Integer.MIN_VALUE意味着它会在几乎所有框架过滤器之前执行包括一些内部初始化逻辑如果你在里面读取特定Header可能还没准备好。6.4 重试或转发时body被提前消费掉这个坑在Retry过滤器场景有完整体现。Retry失败重试时第二次发往下游的请求理论上应该和第一次一模一样但如果第一次转发已经读取了请求体第二次重试时从exchange.getRequest().getBody()读到的是一个已经流尽的空流。表现在下游就是接口返回成功但数据没入库或者第二次请求参数为空报了参数缺失。解决思路是在第一次读取body前把它缓存到内存或临时存储中之后每次重试都用缓存的数据重建请求体。方法类似第5.3节里对request的装饰器包装。这里我想强调一点不是所有接口都适合网关层重试。重试GET请求一般没有副作用POST请求就要评估好幂等性否则重试造成的重复写库比接口偶尔失败更可怕。6.5 修改响应体比改请求体复杂得多别轻易下手很多需求方提过网关统一对响应体加密这类要求我在做技术评估时都会非常慎重。响应体在转发完成后是以DataBuffer流的形式从下游回到网关的如果你想整体替换必须在过滤器里实现响应装饰器重写writeWith方法收集所有数据片段拼成一个完整buffer再执行替换。这个方案的难点不在代码而在异常路径和性能大响应体缓存会占用内存下游异常中断时数据片段不完整如何处理以及如何跟压缩传输配合。如果只是Gateway内部的小流量场景还能接受一旦到了生产大流量环境网关做响应体加工的成本会指数级上升。我的经验是除非是安全合规上的硬性要求否则响应体修改尽量放在调用方或下游服务里网关保持轻量转发。6.6 过滤器报错时的日志要带上下文最后一个坑不算过滤器特性而是排错习惯。过滤器链上的异常如果日志里没有上下文信息很难定位到具体是哪个链路、哪条路由出了问题。我在Trace全局过滤器里打的日志会带上routeId、uri、traceId三件套这样一旦有异常日志可以马上按traceId把一次完整请求在网关层的路径拉出来。顺带一提全局过滤器抛出异常时如果没有统一异常处理客户端会得到一个很笼统的500但网关日志里信息其实很全。建议在全局过滤器异常处理上统一返回格式至少保证认证失败401、限流429、系统异常500能被明确区分。这个看似简单我在多个项目里都发现默认行为并不区分这些状态码排查和联调效率都受影响。结尾过滤器这套东西用是爽但要克制把过滤器这套机制真正跑通之后确实会有一种什么都能在网关做的错觉。我个人在实际项目里的体会是网关过滤器适合收敛那些跨服务都必须执行的横切逻辑比如Trace日志、认证、基础请求头清理不适合承载太多厚重业务逻辑比如复杂的报文转换、业务规则判断这些放网关会拖慢所有请求也让网关排错变成一场灾难。另外一个小技巧是过滤器代码里尽量不要直接依赖具体的路由配置格式多预留配置开关。我后来把很多过滤器都做成了默认开启但可通过YAML关闭的形式这样线上需要临时降级某个过滤器时不用改代码发版只要调整配置就能生效。这算是踩过几次坑之后养成的习惯。如果这篇笔记里的某一段能帮你少踩一个坑我整理它的时间就没白费。后续我还会把网关限流、灰度发布相关的内容单独整理出来到时候再接着聊。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询