
做网关这块时间不短了前面几篇把Gateway的基础路由、全局过滤器和注册中心接入都捋了一遍。这篇我们专门聊GatewayFilterFactory也就是局部过滤器并且拿权限校验这个最常见的业务场景开刀。不是泛泛讲API怎么用而是从“为什么要有局部过滤器”到“怎么用它把登录校验做得又稳又灵活”一遍过完。如果你正在做微服务网关的权限控制或者对Gateway的过滤器链模型一知半解看完这篇应该能直接上手落地。内容里所有代码都是我在模拟项目X里经过压测和线上问题复盘后沉淀出来的不是官方文档的翻译版踩过的坑也都标注了。1. 过滤器工厂体系拆解为什么局部过滤器这么设计1.1 先搞懂“工厂”这两个字的含义GatewayFilterFactory这个命名很容易劝退新手但拆开看其实特别直白。它就是一个“生产过滤器实例的工厂”Spring Cloud Gateway利用Spring Boot的自动配置机制把每一个实现了GatewayFilterFactory接口的Bean都注册成一个“过滤器工厂”而这个工厂的Bean名称去掉GatewayFilterFactory后缀之后就是你在配置里写的那个过滤器名字。举个例子官方自带的AddRequestHeaderGatewayFilterFactory在配置里写的就是AddRequestHeader。你写- AddRequestHeaderX-Request-Foo, bar它就会创建一个带有指定参数的GatewayFilter实例。这种“配置驱动”的设计让我在调整过滤规则时不用改代码重发版改改配置就完事这对运维来说太关键了。搞清楚这一点你才能理解为什么我们自定义局部过滤器时必须用Component把工厂注册到Spring容器里。因为Gateway在启动阶段会扫描所有GatewayFilterFactory类型的Bean建立“工厂名到工厂类”的映射表如果Bean没注册配置里写的名字就解析不出来直接启动报错。1.2 局部过滤器与全局过滤器、拦截器的三角关系我们常说的“局部过滤器”对应GatewayFilter接口它绑定在某一条具体的路由上通过RouteDefinition里的filters字段配置。而全局过滤器对应GlobalFilter接口不绑定路由自动对所有经过网关的请求生效内部通过实现GatewayFilterChain来调用局部过滤器链。实际开发中这两个经常被人混用。我见过有人把所有逻辑都塞进GlobalFilter结果项目里一个全局过滤器上千行改一个接口的权限就要小心翼翼怕影响别的服务。全局过滤器帮我们解决的是“横切关注点”比如接口耗时统计、统一的请求日志。而局部过滤器帮我们解决的是“某条路由的个性化处理”比如订单服务需要校验签名、商品服务只需要登录态这种场景如果用全局过滤就不是“能不能做”而是“每次发版都要祈祷别误伤别人”。拿一棵树来类比全局过滤器是树干所有请求都必须从这儿走局部过滤器是树枝只有走到某个分叉才会触发。这个设计的一致性在于两者最终都会被包装成GatewayFilter实例放进过滤器链里执行模型完全统一。2. 编写第一个局部过滤器工厂5分钟跑通全流程2.1 继承抽象类需要实现哪些方法Spring Cloud Gateway提供了AbstractGatewayFilterFactoryT这个抽象模板类我们自定义过滤器的时候只需要做三件事定义一个配置类、指定泛型类型、实现apply()方法。下面是我在项目X里用过的登录校验过滤器精简版保留了核心链路Component public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactoryAuthGatewayFilterFactory.Config { public AuthGatewayFilterFactory() { super(Config.class); } /** * 通过配置项指定需要跳过的URL白名单 */ Override public ListString shortcutFieldOrder() { return Collections.singletonList(whitelist); } Override public GatewayFilter apply(Config config) { return (exchange, chain) - { String path exchange.getRequest().getURI().getPath(); // 白名单直接放行 if (config.getWhitelist() ! null) { String[] whitelist config.getWhitelist().split(,); for (String prefix : whitelist) { if (path.startsWith(prefix.trim())) { return chain.filter(exchange); } } } // 1. 从请求头拿token ListString tokens exchange.getRequest().getHeaders().getOrDefault(Authorization, Collections.emptyList()); if (tokens.isEmpty()) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 2. 进入校验逻辑通过则放行不通过则返回401 if (!checkToken(tokens.get(0))) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); }; } private boolean checkToken(String token) { // 这里按实际项目接入Redis、JWT或远程校验服务 return !invalid-token.equalsIgnoreCase(token); } public static class Config { private String whitelist; public String getWhitelist() { return whitelist; } public void setWhitelist(String whitelist) { this.whitelist whitelist; } } }看到这里你可能会问为什么apply()方法返回的是GatewayFilter而不是直接写过滤逻辑这恰恰是Gateway设计里非常值得一提的点。GatewayFilterFactory接口的实现类只负责把配置类转换成具体的过滤器实例转换过程可以在请求到达之前完成因此过滤器实例可以在启动阶段就预先创建好。请求真正进来时直接执行filter()方法省掉了每次请求动态解析配置、反射生成实例的开销。这种“配置预解析、实例复用”的模式在高并发下对性能非常友好。2.2 把过滤器注册到路由上两种配置方式过滤器工厂写好之后接下来就是把干活的过滤器挂到路由上。支持两种写法一种是在application.yml里的spring.cloud.gateway.routes.filters段配置一种是使用RouteLocatorBuilder的时候用代码指定。YAML方式spring: cloud: gateway: routes: - id: order-service uri: lb://order-service predicates: - Path/order/** filters: - Authwhitelist/order/list,/order/detail代码方式Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { return builder.routes() .route(order-service, r - r.path(/order/**) .filters(f - f.filter(new AuthGatewayFilterFactory().apply( new AuthGatewayFilterFactory.Config() {{ setWhitelist(/order/list,/order/detail); }} ))) .uri(lb://order-service)) .build(); }YAML方式明显更适合交给运维调整代码方式因为可以做动态路由和更复杂的拼接逻辑所以常用于测试或者路由规则需要程序化生成的场景。我个人在生产环境更倾向YAML理由下面说。提示如果你配置了过滤器但是路由不生效先确认过滤器工厂类名。AuthGatewayFilterFactory在配置里要写Auth而不是AuthGatewayFilterFactory也不要写auth大小写必须严格匹配前缀匹配不区分大小写但后缀匹配严格区分。3. 核心实现细节与原理解读权限校验的完整链路3.1 请求拦截的关键“短路”机制如何运作权限校验过滤器里最核心的就是“放行”和“拦截”两个分支。放行就是调chain.filter(exchange)继续往后走而拦截有两种常见的写法我刚接触Gateway的时候经常弄混。第一种是改状态码然后直接返回exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete();第二种是直接抛异常throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, Unauthorized);这两者的核心区别在于setComplete()方法会直接告诉WebFlux当前请求已经处理完成返回一个已完成的MonoVoid后续的过滤器链将不再执行。而throw抛异常还得靠全局异常处理器兜底一旦你的全局异常处理器没接住返回给客户端的就不是你预期的JSON而是默认的HTML错误页。我在一个项目里遇到过的情况是网关返回的错误信息到了前端再由前端弹出一条很粗糙的“服务异常”提示排查到后面发现就是过滤器里抛了RuntimeException但没人处理。所以我现在校验失败统一走setComplete()自定义响应体既保证链路干净又能精准控制错误返回的格式。如果你想让返回体带上JSON提示可以这么写return exchange.getResponse().writeWith(Mono.just(exchange.getResponse() .bufferFactory() .wrap({\code\:401,\message\:\未登录或登录已过期\}.getBytes(StandardCharsets.UTF_8))));但是别忘了设置响应头Content-Type为application/json;charsetUTF-8否则前端拿到的还是会乱码。3.2 放行后的身份传递改造请求体往下游传校验通过之后网关还要干一件事把解析出的用户身份信息传递给下游服务。因为下游服务不会再重新解析token在大多数设计里这样做是为了避免重复解析的耗时网关直接把身份塞进请求头下游放心信任网关传过来的字段。具体做法是修改ServerHttpRequest的headerServerHttpRequest mutatedRequest exchange.getRequest().mutate() .header(X-User-Id, userId) .header(X-User-Role, role) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build());这里的mutate()方法值得停下来多看两眼。它不是一个修改原对象的操作而是基于原请求克隆出一个新请求对象加上新的请求头之后再放回exchange里。这种不可变对象的修改方式避免了并发场景下多个过滤器同时修改同一个请求实例导致的数据竞争。需要注意的是如果有多个过滤器都有修改请求头的需求它们会按照配置顺序依次对exchange进行mutate后面的过滤器能看到前面过滤器加的头。如果配置顺序不对比如你在Auth过滤器后面又配了别的过滤器想读取X-User-Id读取不到那就是顺序问题。还有一点很多同学在传递身份头时下意识把原始token也透传下去。我建议除非下游明确需要校验原始凭证否则在网关就把Authorization头消费掉、换新的身份头往下游传。这样能减少token泄露面也让下游更安全。3.3 白名单设计一组配置解决大部分场景一个成熟系统的权限校验一定有部分接口是匿名的比如登录接口、验证码接口、回调接口。把这些URL做成白名单数组是网关上最轻量的做法。白名单的设计有几个层次。第一层是我上面示例里简单的前缀匹配只要路径以某个前缀开头就放行适合粗粒度控制。第二层是精确路径匹配第三层是正则匹配。项目里如果对权限要求比较高一般用AntPathMatcher之类的工具做模式匹配支持/order/**这样的写法。我常用的配置结构是这样的filters: - Authwhitelist/auth/login,/auth/captcha,/public/**,/callback/**注意这里的逗号分隔在YAML里会被当成一个字符串所以你在代码里要用split(,)拆开。如果你用了shortcutFieldOrder()还要注意一个细节Gateway的快捷配置写法是按字符串解析的值里有逗号没问题但如果某个白名单路径本身带逗号那就有坑了。我在项目里就遇到过一次回调URL带参数被截断的情况后来干脆改成完整配置方式filters: - name: Auth args: whitelist: /auth/login,/auth/captcha完整配置方式容错率更高推荐后面维护的人读代码也更直观。4. 实战复盘权限改造的真实案例4.1 场景设定与方案选型为了说得更具体就拿模拟项目X来说。这是一个典型的微服务商城系统主要服务有用户服务、商品服务、订单服务、支付回调服务。网关层之前做的是全局限流和日志权限校验分散在各业务服务里自己写。改造的诱因很直接新来的同事在订单服务的过滤器里写漏了一个白名单结果用户查订单列表的时候一直拿不到数据排查了一个多小时才发现是校验服务里把查询接口也给拦了。当时项目组长拍板把权限校验整体上移到网关统一收口。技术选型的时候摆在我们面前的就是文章开头说的岔路口是用全局过滤器还是局部过滤器全局过滤器方案写一个GlobalFilter拦截所有请求、做权限校验。优点是“一处配置处处生效”新接入的服务不需要关心校验逻辑。缺点也很明显有些服务是给内部调用的有些接口是开放的全局展开后就得维护一张不断增长的排除列表最后排除列表比业务代码还长。局部过滤器方案可以针对不同路径的路由挂上不同的过滤器实例不同服务可以有不同的校验规则。比如订单服务校验用户身份和订单归属支付服务多校验一个签名参数。缺点是接入新服务时得想着配过滤器容易漏。最后我们的结论是全局过滤器只做“通用”的事情比如记录日志、链路标识而权限校验用局部过滤器。每个服务路由上明明白白写着Auth过滤器一眼就知道这个服务有没有挂校验出了问题也好排查。4.2 完整配置与单元测试跑通改造后的路由配置长这样spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/user/** filters: - Authwhitelist/user/login,/user/register - id: order-service uri: lb://order-service predicates: - Path/order/** filters: - Authwhitelist/order/list,/order/detail - id: pay-service uri: lb://pay-service predicates: - Path/pay/** filters: - Authwhitelist/pay/notify单元测试这块用WebTestClient或者MockServerWebExchange都可以。如果你没用过MockServerWebExchange我强烈推荐掌握它能在不启动完整Gateway的情况下单独测试过滤器逻辑测试速度非常快Test void testAuthFilterBlockRequestWithoutToken() { AuthGatewayFilterFactory factory new AuthGatewayFilterFactory(); AuthGatewayFilterFactory.Config config new AuthGatewayFilterFactory.Config(); config.setWhitelist(/public/**); GatewayFilter filter factory.apply(config); MockServerWebExchange exchange MockServerWebExchange.builder( MockServerHttpRequest.get(/order/create).build() ).build(); StepVerifier.create(filter.filter(exchange, (e) - Mono.empty())) .verifyComplete(); assertThat(exchange.getResponse().getStatusCode()).isEqualTo(HttpStatus.UNAUTHORIZED); }这里用StepVerifier是WebFlux测试的常规操作因为我们处理的是Reactive流不能像命令式代码那样直接断言后马上看结果verifyComplete()会阻塞直到整个流执行完。4.3 压测表现与性能心得改造完成后跑了一轮压测10个并发线程持续压5分钟网关CPU占用和之前相比几乎没有变化。这是符合预期的因为我之前担心的性能瓶颈不是过滤器本身而是过滤器里有没有做同步阻塞操作。我在代码里注释过checkToken方法“按实际项目接入Redis或JWT”这里补充一个重要经验如果在Reactive环境下你用了String token redisTemplate.opsForValue().get(key)这会阻塞Event Loop线程压测一上去CPU立刻飙到90%以上而用了ReactiveStringRedisTemplate的非阻塞调用CPU基本平稳。不光是Redis跟数据库打交道也一样。网关层做校验时尽量用异步客户端实在没法异步的比如调远程接口可以扔到boundedElastic()线程池去执行避免阻塞Netty的Event Loop线程return Mono.fromCallable(() - checkTokenSync(token)) .subscribeOn(Schedulers.boundedElastic()) .flatMap(isValid - { if (!isValid) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } return chain.filter(exchange); });之前在测试环境没见过问题是因为并发量不够。一旦线上流量涨起来了同步阻塞调用带来的后果就是线程池耗尽、请求排队、接口超时雪崩。这块真的要在设计阶段就绷紧弦。5. 常见问题与排查技巧实录5.1 过滤器没生效这个算是我被问得最多的问题。写了Auth过滤器启动Gateway也不报错但请求就是不走校验逻辑。排查步骤一般是先看路由ID是不是和请求路径对应上了。Gateway的路由匹配是predicates决定的如果请求没匹配到这条路由那过滤器链根本就不会触发这种情况下你看不到任何日志。打开logging.level.org.springframework.cloud.gatewayDEBUG看请求命中的是哪个RouteDefinition。第二常见的场景是过滤器配在了一个永远不会执行到的分支上。比如你同时配了Path/order/**和Path/user/**两条路由但请求路径是/order/1你却在user-service的路由上挂了Auth那自然不会执行。第三检查你是不是不小心把GatewayFilterFactory的Bean定义成了多个。如果一个工厂类被注册了两次Gateway按名称查找时会抛出异常或者找到错误的实例。5.2 配置不识别、类转换异常如果启动时看到类似Unable to find GatewayFilterFactory with name Auth八成是类名和配置名不匹配。记住规则类名是AuthGatewayFilterFactory配置名是Auth两者要严格对应。还有一种情况是Component注解没加工厂没进容器。另一种隐蔽问题配置类的属性名和配置项对不上。Config类里必须有对应的setter因为Gateway在做配置绑定时用的是JavaBean规范。如果你写的是private ListString whitelist;但配置里传的是逗号分隔的字符串启动时就会因为类型转换失败而报错。所以我示例里的Config.whitelist用的是String配合shortcutFieldOrder()的字符串切割方案。5.3 让你少走弯路的排错小技巧给过滤器加上阶段日志。在apply()里加一行log.info(Auth filter applied for route: {}, config)在filter()里加“进入校验、校验通过、校验失败”三个关键节点日志。权限类的故障定位日志永远是最快的路径。还有一个小技巧给过滤器工厂定义一个order字段。GatewayFilter可以通过实现Ordered接口来指定执行优先级如果一条路由上挂了多个过滤器它们的执行顺序就由这个order决定。我一般把权限校验设为最高优先级放在路由转发和限流之前先拦住非法请求再做后续处理。最后聊一个Optional的坑。exchange.getRequest().getHeaders().getOrDefault(Authorization, Collections.emptyList())里Header的值是一个ListString你取的时候一定不能直接get(0)因为请求头可能没有或者有单值。很多初学者在token为空时会抛IndexOutOfBoundsException过滤器直接500。养成先判空再取值的习惯哪怕代码看起来啰嗦一点安全系数高很多。6. 一些踩坑后才能总结出的思路回到开头那句“权限校验”如果你准备在网关层落地我强烈建议别一上来就照着网上的代码抄。先盘一下自己的服务结构把接口分成三类绝对公开、登录可访问、必须鉴权。再根据分类设计白名单、身份头透传规则和错误返回结构。这步想清楚了写过滤器就是体力活。过滤器这块我踩过最大的坑是局部过滤器和全局过滤器的边界没想清楚。一开始图省事全写全局过滤后来维护成本飙升。后来统一规则全局过滤器只做平台级横切逻辑局部过滤器做业务级个性化逻辑两者通过order协调互不干扰。这次改造之后新服务接入网关只需要在路由上加一行Auth配置测试同学找我问“这个接口为什么被拦”的工单数量明显下降了。最后再分享一个小技巧写权限过滤器时把“拿token解析用户信息”和“判断用户是否有权限访问”拆成两个独立的方法。前者可以复用后者跟具体业务绑定。这样将来你要调整某个接口的访问策略不需要动过滤器主体只改权限判断逻辑就行。