Spring Cloud Gateway 局部过滤器实现权限校验完全指南
发布时间:2026/10/23 19:33:15 锦皓数字建站

网关系列写到第四篇终于可以聊聊权限校验了。前面几篇我们把路由和谓词都过了一遍但说实话光有路由和谓词网关只算是个“聪明的转发器”真正让它在微服务架构里不可替代的是过滤器。今天就集中梳理 GatewayFilterFactory 这个局部过滤器工厂从内置的常用工厂怎么选到自定义一个权限校验工厂再到和限流、熔断这些过滤器如何配合一次讲透。看完这篇你至少能回答这几个问题局部过滤器和全局过滤器到底怎么选自定义 GatewayFilterFactory 的标准写法是什么权限校验在网关层到底校验到哪一层才算够。适合已经搭好 Gateway 基础、正准备做统一鉴权的同学。1. 网关权限校验的两条路线为什么我优先用局部过滤器1.1 全局过滤器与局部过滤器的实质区别Gateway 里其实有两套过滤器体系很多人一开始会混淆。GlobalFilter 是全局过滤器实现了这个接口的 Bean 会作用于所有路由。它不需要挂到某个路由上启动时就全量生效适合做全链路日志、请求 ID 埋点、跨域处理这类通用横切逻辑。GatewayFilterFactory 则是“过滤器工厂”它的产物是一个 GatewayFilter。这种过滤器通过路由配置显式挂载Spring Cloud Gateway 根据 Bean 名约定把工厂类名变成配置里的过滤器名。它只对绑定的路由生效所以叫局部过滤器。一句话区分全局过滤器是“无差别覆盖”局部过滤器是“按路由精准投放”。权限校验这种逻辑往往不同服务不同路径规则千差万别全局一刀切反而难维护。还有一个很多人忽略的点——执行顺序。全局过滤器实现 Ordered 接口可以控制彼此先后局部过滤器则跟随路由配置中的装配顺序它们最终会混在一起按 order 排序执行。这个细节我放到第四章详细讲这里先记住一个结论权限校验放在局部过滤器里更可控、更好调整。1.2 内置 GatewayFilterFactory 实用清单虽然主题是自定义过滤器但动手之前一定要先看看官方的内置工厂。很多时候我们根本不用写代码几个内置过滤器一拼就够用了。过滤器工厂作用典型场景AddRequestHeader为请求增加请求头写入内部标记、透传链路 IDRemoveRequestHeader移除指定请求头抹掉客户端传入的危险头AddResponseHeader为响应增加头统一 CORS 头、安全头StripPrefix去除路径前 N 段去掉 /api 前缀PrefixPath为路径加前缀老服务路径迁移SetPath重写请求路径网关路由收敛RedirectTo重定向短链、强制跳转Retry请求失败重试瞬时网络抖动RequestSize限制请求体大小上传接口防护RequestRateLimiter按维度限流防刷、配额控制CircuitBreaker熔断降级下游故障兜底SaveSession保存 WebSession有状态会话这些工厂在官方文档里都有但实际项目里我发现很多同学只用到 StripPrefix 和 RequestRateLimiter。建议对每个都建个 Demo 路由试一遍理解参数含义后面自定义时才明白框架的设计思路。1.3 为什么权限校验优先考虑局部过滤器先说不建议用全局过滤器的场景。假设你有三条路由管理端接口、用户端接口、开放 API。管理端要校验管理员 Token用户端只要校验普通登录态开放 API 要走签名验签。如果用一个 GlobalFilter 写代码里全是 if/else 判断请求路径越写越长加一条路由就要动一次全局过滤器。局部过滤器天然适合这种差异化的场景因为过滤器工厂可以参数化。同一个 Auth 过滤器在管理端路由上配置严格校验模式在用户端路由上配置宽松模式在开放 API 路由上配置验签模式。代码只写一份参数通过路由配置注入行为却完全不同。全局过滤器更适合做“无差别的事”打印请求日志、注入 traceId、统计流量。把鉴权逻辑塞进全局过滤器短期看省事后期每个路由做差异化时就要开始痛苦的拆代码了。除非你的网关真的只有一条路由否则我都建议权限校验走局部过滤器。2. 自定义 GatewayFilterFactory从零手写一个权限校验过滤器2.1 先设计清楚这个过滤器到底要做什么动手写代码之前先把职责列清楚。我这个权限过滤器要干五件事维护白名单白名单内的路径直接放行从 Authorization 头里提取 Token并校验格式当配置开启签名校验时校验请求签名校验 Token 是否有效、是否过期校验通过后把用户 ID 写入请求头转发给下游服务。为什么需要白名单因为登录接口、注册接口、健康检查这些路径根本没有 Token 可用如果过滤器一视同仁全部拦截登录入口就死循环了。这个设计是所有网关鉴权的基础先想清楚再写代码会顺手很多。2.2 配置类与参数绑定GatewayFilterFactory 的精髓在于参数绑定。yml 里配置过滤器时写的那些参数最终会映射到工厂内部的一个 Config 类上。所以第一步先定义 Config 类。public static class Config { private ListString whiteList new ArrayList(); private boolean verifySignature false; private String userIdHeader X-User-Id; public ListString getWhiteList() { return whiteList; } public void setWhiteList(ListString whiteList) { this.whiteList whiteList; } public boolean isVerifySignature() { return verifySignature; } public void setVerifySignature(boolean verifySignature) { this.verifySignature verifySignature; } public String getUserIdHeader() { return userIdHeader; } public void setUserIdHeader(String userIdHeader) { this.userIdHeader userIdHeader; } }两点注意事项。第一Config 类必须是 public static 内部类并且要有无参构造函数否则 Spring 无法完成自动绑定。第二如果你的过滤器希望支持 yml 简写形式还需要重写 shortcutFieldOrder 方法Override public ListString shortcutFieldOrder() { return Arrays.asList(whiteList, verifySignature, userIdHeader); }这个方法定义了简写参数的顺序。比如- Auth/api/user/login,true,X-User-Id会被按照这个顺序解析到 Config 里。不过我的建议是白名单这种 List 类型参数不要用简写多个路径用逗号分隔时很容易踩解析的坑后面第五章会详细说。2.3 核心 apply 方法与完整过滤器代码接下来是重头戏apply 方法。这个方法接收一个 Config返回一个 GatewayFilter。GatewayFilter 的核心逻辑写在 filter 方法里通过 exchange 可以拿到请求和响应对象。下面是一份完整的代码示例为了方便阅读我使用了同步的 StringRedisTemplate生产环境建议换成响应式 Redis这点后面会展开。Component public class AuthGatewayFilterFactory extends AbstractGatewayFilterFactoryAuthGatewayFilterFactory.Config { private final StringRedisTemplate redisTemplate; public AuthGatewayFilterFactory(StringRedisTemplate redisTemplate) { super(Config.class); this.redisTemplate redisTemplate; } Override public ListString shortcutFieldOrder() { return Arrays.asList(whiteList, verifySignature, userIdHeader); } Override public GatewayFilter apply(Config config) { return (exchange, chain) - { ServerHttpRequest request exchange.getRequest(); String path request.getURI().getPath(); // 第一层白名单直接放行 boolean inWhiteList config.getWhiteList().stream() .anyMatch(pattern - path.startsWith(pattern)); if (inWhiteList) { return chain.filter(exchange); } // 第二层Token 存在性与格式校验 String token request.getHeaders().getFirst(Authorization); if (token null || !token.startsWith(Bearer )) { return unauthorized(exchange, 缺少Token或格式错误); } token token.substring(7); // 第三层Token 状态校验这里用 Redis 判断是否有效 String userId redisTemplate.opsForValue().get(login:token: token); if (userId null) { return unauthorized(exchange, Token无效或已过期); } // 第四层可选签名校验 if (config.isVerifySignature()) { String signature request.getHeaders().getFirst(X-Signature); if (!checkSign(request, signature, userId)) { return unauthorized(exchange, 签名校验失败); } } // 第五层透传用户信息并防伪造 ServerHttpRequest newRequest request.mutate() .headers(httpHeaders - { httpHeaders.remove(config.getUserIdHeader()); httpHeaders.set(config.getUserIdHeader(), userId); }) .build(); ServerWebExchange newExchange exchange.mutate().request(newRequest).build(); return chain.filter(newExchange); }; } private MonoVoid unauthorized(ServerWebExchange exchange, String message) { ServerHttpResponse response exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().setContentType(MediaType.APPLICATION_JSON); byte[] bytes ({\code\:401,\message\:\ message \}) .getBytes(StandardCharsets.UTF_8); return response.writeWith(Mono.just(response.bufferFactory().wrap(bytes))); } private boolean checkSign(ServerHttpRequest request, String signature, String userId) { if (StringUtils.isEmpty(signature)) { return false; } String body userId userId path request.getURI().getPath(); String expected DigestUtils.md5DigestAsHex(body.getBytes(StandardCharsets.UTF_8)); return expected.equals(signature); } }有几个细节必须强调第一chain.filter(exchange)是放行的唯一入口如果条件不满足却没有调用它请求会一直挂着不返回。第二失败响应里必须调用response.writeWith(...)并返回这个 Mono同时不要再继续调用chain.filter否则响应会被写两次。第三透传用户信息时要先 remove 再 set目的是把外部客户端伪造的同名请求头覆盖掉避免下游被欺骗。2.4 注册 Bean 并在路由中生效类上已经有 Component 注解Spring Boot 启动时会自动注册这个过滤器工厂。但这里有一个命名约定务必记牢Spring Cloud Gateway 是根据 Bean 的名字去掉 “GatewayFilterFactory” 后缀来匹配 yml 配置的。也就是说类名AuthGatewayFilterFactory注册后的 Bean 名是authGatewayFilterFactory去掉后缀变成authyml 里配置过滤器时写Auth就能匹配上。如果你把类名改成AuthFilterFactory那配置名就要写成AuthFilter。我见过不少同学在类名上随意发挥结果过滤器怎么都不生效卡了半天。如果你不喜欢 Component 自动注册也可以在用配置类手动 new效果一样Configuration public class GatewayConfig { Bean public AuthGatewayFilterFactory authGatewayFilterFactory(StringRedisTemplate redisTemplate) { return new AuthGatewayFilterFactory(redisTemplate); } }yml 里的路由配置如下spring: cloud: gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/api/user/** filters: - StripPrefix1 - name: Auth args: whiteList: /api/user/login,/api/user/register,/actuator/health verifySignature: true userIdHeader: X-User-Id这里同时挂了两个局部过滤器StripPrefix 先执行把 /api/user/** 的 /api 去掉然后 Auth 执行按参数校验白名单、Token 和签名。filter 列表的顺序就是过滤器链的加入顺序这个马上会在第四章展开。3. 权限校验的细节设计与方案取舍3.1 校验的四个层次很多刚接触网关鉴权的同学以为“校验 Token”就是“查一下 Redis 里有没有这个 key”其实一个成熟的权限校验至少要分四层。第一层是白名单。不需要登录的路径包括登录接口、注册接口、验证码、健康检查、静态资源等必须显式放行。这里要注意匹配方式不要用path.startsWith(/api/user/login)这种写法因为/api/user/login_admin也会被误放行。更稳妥的做法是使用 PathPattern 或者 Spring 的 AntPathMatcher精确匹配路径规则。第二层是 Token 的存在性和格式。Authorization 头有没有、是不是 Bearer 开头、Token 切出来之后能不能做 Base64 解码。这层校验非常快不涉及网络请求主要用来拦截明显不合法的请求。第三层是签名校验。适合开放 API 场景防止请求参数被篡改。网关验签要共享密钥签名规则要前后端对齐这是所有层次里最灵活也最容易被忽略的。不需要每个路由都开做成开关按需启用就好。第四层是 Token 的有效状态。Token 是否过期、用户是否被踢下线、密码修改后旧 Token 是否失效。这层必须依赖存储用 Redis 是最常见的做法。3.2 无状态 Token 还是 Redis 状态校验在实际项目里Token 方案的选择直接影响权限校验的实现方式。我遇到过不少团队一开始用纯 JWT后来发现“踢人”和“登出失效”根本做不了又回来改造。方案优点缺点适用场景纯 JWT校验快不需要存储无法主动失效泄漏难追责开放 API、第三方合作Token Redis登出立即可用支持踢人每次请求多一次 Redis 交互内部系统、后台管理混合方案JWT 负责签名Redis 做黑名单实现复杂中大型平台如果你做的是公司内部系统我强烈建议 Token Redis。因为内部系统的“登出”和“多端登录控制”是刚需纯 JWT 很难优雅支持。Redis 的交互成本可以接受只要做好高可用和降级策略。Redis 挂掉怎么办这也要提前想好。网关可以做降级校验失败时记录日志并放行保证业务可用同时触发告警或者直接返回 503宁可不可用也不放行。这个取舍取决于公司的容错倾向没有绝对正确的答案但一定要有预案。3.3 用户信息透传与防伪造网关校验通过后下游服务怎么知道当前用户是谁常规做法是把 userId 写进请求头随请求一起转发。但这里有一个严重的隐患外部客户端完全可以自己伪造一个X-User-Id头传给网关如果网关不做处理下游服务一旦信任了这个头就会造成越权。所以网关在透传用户信息之前必须先把外部传入的同名头清理掉再写入真实的 userId。ServerHttpRequest newRequest request.mutate() .headers(httpHeaders - { httpHeaders.remove(config.getUserIdHeader()); httpHeaders.set(config.getUserIdHeader(), userId); }) .build();哪怕 Spring 的 mutate 机制本身会覆盖同名头我也建议显式 remove 一次代码意图更清晰也不依赖框架升级后的行为变化。还有一个点下游服务要约定“只信任来自网关的请求头”。如果服务被内网其他服务直接调用绕过网关同样要能识别出没有 X-User-Id 头并拒绝或要求显式身份标识。网关只是收敛了流量入口真正的信任边界还要靠服务端配合。3.4 失败响应的全局统一处理权限校验失败时网关返回什么响应直接影响前端的对接体验。默认的响应是一段 HTML 报错页前后端分离项目拿到这种响应非常痛苦。统一 JSON 结构是基本操作比如{code:401,message:Token无效或已过期}。在此基础上还可以加上path和timestamp方便排查。响应头要显式设置Content-Type: application/json;charsetUTF-8否则中文很可能乱码。401 和 403 要区分清楚未登录返回 401已登录但权限不足返回 403。我见过一些项目图省事一律返回 401导致前端无法区分“需要跳登录页”和“提示无权限”体验很差。另外不要在响应里暴露内部异常信息比如 Redis 连接失败、空指针堆栈之类的这些应该记录在网关日志里而不是展示给客户端。4. 过滤器组合与执行顺序和限流、熔断放在一起怎么调4.1 局部过滤器之间的执行顺序从哪来如果你在一个路由上挂了多个局部过滤器它们的执行顺序是怎么定的答案是默认按 yml 配置顺序执行。但这里有个细节容易被忽略。Spring Cloud Gateway 最终会把路由上的局部过滤器和全局过滤器混在一起排序全局过滤器可以实现getOrder()返回一个 int 来控制顺序。局部过滤器如果没实现 Ordered 接口它的 order 就是 0排序时靠稳定排序保持原来的相对顺序。换句话说yml 里的顺序在大多数情况下有效但如果你在某处改了全局过滤器的 order有可能会插到局部过滤器链中间打乱你预期的执行顺序。所以我的习惯是对关键过滤器显式实现 Ordered 接口给每个过滤器划定 order 区间而不是完全依赖配置顺序。比如权限过滤器希望优先执行可以实现Override public int getOrder() { return -100; }数值越小越靠前。这样就算混入其他过滤器Auth 也能排在最前面。4.2 与 RequestRateLimiter 组合实现限流限流和鉴权经常一起出现。一个常见的问题是先限流还是先鉴权答案是看限流维度。如果按 IP 限流那 RequestRateLimiter 可以放在 Auth 前面因为还没解析用户身份就可以按 IP 计数这样能拦截大量非法请求减轻鉴权压力。如果按用户维度限流那就必须先执行 Auth拿到 userId 之后KeyResolver 才能从请求头里取到用户标识。组合配置长这样filters: - name: RequestRateLimiter args: redis-rate-limiter.replenishRate: 10 redis-rate-limiter.burstCapacity: 20 key-resolver: #{ipKeyResolver} - name: Auth args: whiteList: /api/user/login,/api/user/register,/actuator/health这个配置是 IP 限流在前、鉴权在后。对于登录接口来说限流能防止别人用脚本暴力重试即使登录接口在白名单里限流依然有效这是很实用的小技巧。4.3 与 CircuitBreaker 组合实现降级返回下游服务不稳定时网关需要快速失败而不是让前端一直等。CircuitBreaker 过滤器就是干这个的。它的基本用法是配一个 fallbackUri当调用下游失败时转发到本地的一个降级接口filters: - name: CircuitBreaker args: name: userServiceCB fallbackUri: forward:/fallback这里有一个重要注意点CircuitBreaker 通常放在 Auth 之后、靠近转发的位置。如果放在 Auth 前面鉴权失败返回的 401 也会被熔断器统计导致下游一抖动就把健康的鉴权逻辑也熔断了误伤面会很大。Gateway 提供的这个降级能力配合权限校验过滤器可以组合出一套比较完整的网关防护链限流挡恶意流量鉴权挡非法请求熔断挡住下游故障三件事各司其职互不干扰。5. 常见问题与排查技巧实录5.1 过滤器不生效的三大自查点权限过滤器写完但完全不执行这是最常见的故障没有之一。我的排查顺序基本固定依次看三处。第一Bean 有没有注册。用了 Component 的话检查类所在的包有没有被主类扫描到用了 Bean 的话检查配置类是否被加载。另外一个低级错误是类名后缀不对不是 GatewayFilterFactory 结尾导致 yml 里写的名字匹配不上。第二路由上有没有挂过滤器。我见过有人把过滤器写到了别的路由下面当前请求路径命中的是另一条路由自然就绕过去了。用- Path/api/user/**去对照实际请求路径一步步缩小范围。第三yml 配置的过滤器名拼写。Auth 对应的 Bean 是 authGatewayFilterFactory如果你写成了AuthFilter框架在上下文里找不到对应类型的 Bean启动时会直接报错如果启动没报错但过滤不执行多半是命中错误。排查时可以临时在过滤器入口打印一行日志确认代码真的进来了。这一步能筛掉绝大多数环境问题。5.2 getConfig() 返回 null 和参数绑定问题自定义 GatewayFilterFactory 时最经典的空指针就是 config 为 null。原因基本只有一个构造方法里没有调用super(Config.class)。AbstractGatewayFilterFactory需要知道 Config 的类型才能完成参数自动绑定。不传的话apply 拿到的 config 就是个 null访问里面的白名单列表立刻空指针。这个错误很隐蔽因为代码编译期不会暴露。还有一类问题是参数绑不上。比如我上面提到的简写顺序当你用- Auth/api/user/login,true,X-User-Id这种写法时shorcutFieldOrder 的顺序必须和参数一一对应多一个少一个都会解析异常。尤其是 List 参数简写时很容易因为路径里有逗号造成误解。我的建议是自定义过滤器一律用完整写法name 加 args不要用简写。可读性好也不容易踩绑定的坑。除非你的参数全是简单类型且数量稳定。5.3 性能隐患不要在过滤器里做同步阻塞操作Gateway 底层是 Netty 加 WebFlux线程模型和传统 Tomcat 完全不同。它适合的是非阻塞 IO事件循环线程非常宝贵如果在过滤器里用同步 StringRedisTemplate 查 Redis一个阻塞就会拖住整个线程高并发下网关性能直线下降。我这里给出的完整示例为了让大家看懂用了同步的 StringRedisTemplate生产环境如果要查 Redis请换成 ReactiveStringRedisTemplate核心写法差异不大return reactiveRedisTemplate.opsForValue().get(login:token: token) .flatMap(userId - { ServerHttpRequest newRequest request.mutate() .header(config.getUserIdHeader(), userId) .build(); ServerWebExchange newExchange exchange.mutate().request(newRequest).build(); return chain.filter(newExchange); }) .switchIfEmpty(Mono.defer(() - unauthorized(exchange, Token无效或已过期)));如果团队对响应式编程还不熟悉另一个务实方案是网关只做轻量校验比如签名和 Token 格式登录状态这种需要查存储的校验下沉到业务服务。网关保持轻量化也是很多团队的最终选择。5.4 问题速查表现象可能原因处理方案过滤器完全不执行未注册 Bean、路由没挂、名字拼写错误按 5.1 三步自查config 为 null构造方法没调用 super(Config.class)补上父类构造调用白名单参数解析失败简写参数与 shortcutFieldOrder 不匹配改用 name args 完整写法响应中文乱码编码不是 UTF-8写入时显式使用 StandardCharsets.UTF_8401 响应写了两遍失败分支没有返回又调用了 chain.filter失败时直接 return writeWith 的结果网关高并发下 CPU 飙高过滤器里做同步阻塞 IO改响应式 Redis或把重校验下沉另外补充一个调试技巧把logging.level.org.springframework.cloud.gatewayDEBUG打开可以看到每个路由匹配了哪些过滤器、过滤器的执行顺序排查配置问题效率能提升很多。我调试过滤器顺序时基本都靠它。我个人在网关权限这块的经验是不要一上来就追求“万能过滤器”。先把内置工厂玩熟再动手写自定义过滤器。自定义时优先做白名单和 Token 校验验签这种开销大的能力做成可配置开关按路由按需启用。网关层做权限校验核心不是“拦截了多少非法请求”而是让下游服务默认信任网关把公共的安全逻辑收敛到一个入口。后面如果想继续扩展可以考虑把白名单和密钥放到配置中心做动态热更新在过滤器里每次读取配置再决定是否放行。那就比单纯写过滤器更有意思了下一篇有机会再细聊。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。