Spring Cloud Gateway网关层登录校验:GlobalFilter与GatewayFilter实战解析
前阵子给项目里一个新拆出来的订单服务加登录校验点开代码一看又是那套从其他服务里复制过来的token解析逻辑。说实话那一刻我是有点崩溃的。微服务拆了十几个以后每个服务都维护一份属于自己的“登录校验”校验逻辑还不完全一样A服务放行的请求B服务可能直接拒绝。后来我下定决心把这一层统一挪到了网关用Spring Cloud Gateway的自定义过滤器把登录校验这件事收敛到一个入口世界总算清净了。这篇继续SpringCloud系列教程第十四篇重点聊网关层的登录校验和过滤器机制。具体会讲清楚两件事一个是GlobalFilter全局过滤器怎么写、为什么它能拦下所有经过网关的请求另一个是GatewayFilter局部过滤器怎么用、它和全局过滤器的分工边界在哪里。如果你正准备把散落在各服务里的登录校验收敛到网关或者正在背微服务面试题想搞懂网关过滤器的底层逻辑这篇可以对照着代码直接落地。1. 网关层为什么该接住登录校验这块硬骨头先聊一个经常被忽略的问题登录校验放在网关层到底是为了什么很多教程直接告诉你怎么写过滤器但不说清楚设计动机就容易出现“代码照着敲了换个场景就不会用”的情况。1.1 分散校验导致的问题远远不止重复代码微服务拆到一定程度后业务服务之间往往是互相调用的订单服务要调用户服务用户服务要调积分服务。如果每个服务都在自己的代码里写一套登录校验会遇到几个很现实的问题重复代码爆炸。每个新服务落地时都要把token解析、过期判断、用户信息提取这一套逻辑复制一遍稍微改一个字段就要全局同步。校验标准不统一。有的服务校验了token签名有的只判断了token非空有的连过期时间都没查安全隐患就藏在这种不一致里。安全策略改动成本高。比如公司规定所有接口必须强制校验某个请求头你需要在每一个服务里改代码然后逐个发版。新增服务容易漏。只要有一个服务忘了加校验逻辑这个服务就成了整个系统的后门而你还未必能在测试阶段发现。我在实际项目里见过最典型的例子内部管理后台对接了一个对外API因为下游某个服务没做登录校验导致未登录用户可以直接调接口拿数据。排查到最后问题根源不是代码漏洞而是“该由谁统一负责校验”这件事根本没定下来。1.2 网关集中校验的三个核心收益把登录校验收到网关层之后上面这些问题会变成另一种画风流量入口唯一。所有外部请求必须经过网关才能到达后端服务校验逻辑在入口处一次性完成相当于小区只有一个大门保安在大门处查证不需要每栋楼再设一道岗。与业务逻辑解耦。服务的代码里不再出现任何token解析相关的逻辑服务只关心“既然请求能到我这说明身份已经验过了”然后直接信任网关写入的用户信息头。可动态调整。白名单、需要放行的路径、校验强度这些配置可以收口到配置中心改完不用重新发版各个业务服务。这里要特别强调一个边界网关做的是身份认证不是业务鉴权。认证解决“你是谁”的问题鉴权解决“你能不能做这件事”的问题。比如网关确认了userId10086的请求是合法的但userId10086能不能查看订单号20260101的详情这是订单服务自己要判断的。把鉴权也塞进网关会让网关变得臃肿且无法维护这是新手最容易踩的方向性错误。1.3 网关过滤器的三层关系先搭个框架Spring Cloud Gateway的过滤器体系可以分成三层来看在内置的过滤器链路中GlobalFilter和GatewayFilter会统一被封装进过滤器链。为了让后面的代码好理解我先用大白话描述这个框架普通的路由过滤器GatewayFilter参与链路的方式无需在 WebFlux 运行期仅作为一个具体配置。实际运行时通过一个链执行过滤逻辑。官网示例中将部分内置过滤器的行为描述为参与链路执行。上面这条仅作技术解释不是说这里就应提到官网的“示例”结合原文的语境普通过滤器只对匹配了特定路由的请求生效。比如你只有订单服务的路由配了这个过滤器那么用户服务、积分服务的请求就不会经过它。全局过滤器GlobalFilter对所有经过网关的请求生效不需要在路由配置里显式声明只要在Spring容器里注册成Bean它就自动加入链路。两者的连接点Spring Cloud Gateway在真正执行时会把匹配到的GatewayFilter列表和所有GlobalFilter实现类整合成一个过滤器链按照Order值排序后逐个执行。因为GlobalFilter默认对所有请求生效所以常见的登录校验、跨域处理、请求日志都适合做成GlobalFilter而一些只针对特定服务或特定路径的定制逻辑更适合用GatewayFilter。搞清楚这个框架后面写代码才不会迷糊。2. 搭好带网关的微服务骨架依赖、路由与注册中心先把你手头的项目状态对齐一下。我默认的场景是你已经有一套微服务骨架里面有注册中心我用的是Nacos、配置中心以及若干业务服务。这一节专门把网关模块从零搭起来因为后面所有过滤器的效果都要跑在它上面才能验证。2.1 依赖选型和版本对齐网关模块本身也是一个Spring Boot应用只是不引入WebMVC而是引入WebFlux响应式网关。创建一个新的module在pom里加上关键依赖dependencies !-- 网关核心 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency !-- 服务注册发现 -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency !-- 可选配置中心 -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-config/artifactId /dependency /dependencies版本对齐这里必须提醒一句Spring Cloud Gateway是跟随Spring Cloud版本走的你如果还在用Spring Boot 2.x就用spring-cloud-starter-gateway的2.x版本如果已经升级到Spring Boot 3.x网关组件也跟着到了4.xAPI写法基本一致但部分依赖传递会有差异。我自己现在用的组合是Spring Boot 3.2 Spring Cloud 2023.0.x Spring Cloud Alibaba 2023.0.x这套组合跑网关很稳。注意千万别在网关模块里顺手引入spring-boot-starter-web否则网关启动会直接报错甚至起不来。因为Gateway基于WebFluxSpring MVC的DispatcherServlet会干扰响应式路由装配。这个坑我见过很多人踩。2.2 网关的基础配置与路由规则下面的application.yml是一个基本的网关配置骨架server: port: 8080 spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: discovery-locator: enabled: true # 通过注册中心自动创建路由 routes: - id: auth-service uri: lb://auth-service predicates: - Path/api/auth/** filters: - StripPrefix1 - id: order-service uri: lb://order-service predicates: - Path/api/order/** filters: - StripPrefix1逐个解释这里的关键点lb://auth-service表示从注册中心找到名为auth-service的实例列表然后由Gateway内部的负载均衡过滤器LoadBalancerClientFilter选择一个实例转发。这意味着你不必在配置里写死服务地址服务实例上下线网关会自动感知。Path/api/auth/**是断言Predicate用来匹配请求路径。只有匹配到这个路径的请求才会走这条路由规则。StripPrefix1是网关内置的一个局部过滤器作用是把请求路径的第一段前缀去掉。比如客户端请求/api/auth/loginStripPrefix1后网关转发给auth-service的路径是/login。这个设计里前缀只是网关路由定位用的业务服务本身不关心它叫/api还是/openapi。搭完骨架之后启动网关和至少一个业务服务请求能顺利转发后面做登录校验才有对象。2.3 网关注册到Nacos的注意事项网关也需要注册进注册中心这样前端或其他内网服务才能通过服务名访问网关。注册配置就一行server-addr的事但有一个细节值得注意网关的spring.application.name决定了它在注册中心里的服务名如果多个环境dev/test/prod共用一个Nacos最好在名字后面加上环境后缀或者通过namespace隔离避免路由错乱。我在把网关接入已有Nacos时就遇到过测试环境的网关路由到了生产环境的业务实例上原因就是两边服务名相同、namespace没隔离排查了很久才意识到是环境边界的问题。3. GlobalFilter在全局链路上加一道自定义关卡骨架搭好之后进入这一篇的正题写一个GlobalFilter实现登录校验。这一节会有完整代码也会讲清楚为什么代码要这么组织。3.1 GlobalFilter的执行机制和内建链路Spring Cloud Gateway处理请求时先通过GatewayHandlerMapping找到匹配的Route然后组装出过滤器链。链路上默认有一批内置GlobalFilter比如RouteToRequestUrlFilter把路由的目标地址封装进请求属性LoadBalancerClientFilter处理lb://前缀的服务发现与负载均衡NettyRoutingFilter真正发起HTTP转发ForwardRoutingFilter处理forward:前缀的转发这些内置过滤器已经做完了“把请求从网关送到服务”的所有重活。我们自定义GlobalFilter要做的事情是把自己插入到这条链路的关键节点上比如在转发之前拦下来做身份校验。Spring Cloud Gateway内部最终会通过GatewayFilterAdapter把GlobalFilter适配成GatewayFilter再和路由下的GatewayFilter一起排序执行所以从调用链的角度看两者是协同的只是声明方式不同。3.2 AuthGlobalFilter完整实现下面这个过滤器是我在项目里收敛登录校验后沉淀下来的基础版本直接可以复制修改Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private static final ListString WHITE_LIST Arrays.asList( /api/auth/login, /api/auth/register, /api/auth/captcha ); Autowired private JwtUtil jwtUtil; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1. 白名单路径直接放行 String path exchange.getRequest().getURI().getPath(); if (WHITE_LIST.contains(path)) { return chain.filter(exchange); } // 2. 从请求头获取token String token exchange.getRequest().getHeaders().getFirst(Authorization); if (token null || !token.startsWith(Bearer )) { return unauthorizedResponse(exchange, 缺少有效的访问凭证); } token token.substring(7); // 3. 解析token校验签名和过期时间 try { Claims claims jwtUtil.parseToken(token); Integer userId claims.get(userId, Integer.class); String username claims.get(username, String.class); // 4. 把用户信息写入请求头继续向下游传递 ServerWebExchange newExchange exchange.mutate() .request(exchange.getRequest().mutate() .header(X-User-Id, String.valueOf(userId)) .header(X-User-Name, URLEncoder.encode(username, StandardCharsets.UTF_8.name())) .build()) .build(); return chain.filter(newExchange); } catch (Exception e) { return unauthorizedResponse(exchange, 登录状态已失效请重新登录); } } private MonoVoid unauthorizedResponse(ServerWebExchange exchange, String message) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); String body {\code\:401,\message\:\ message \}; DataBuffer buffer exchange.getResponse().bufferFactory() .wrap(body.getBytes(StandardCharsets.UTF_8)); return exchange.getResponse().writeWith(Mono.just(buffer)); } Override public int getOrder() { return -100; } }这段代码看起来不长但每一步都是有用意的。我拆开讲白名单先行登录、注册、验证码这类接口在调用时还没有身份必须放行。白名单判断放在最前面不是为了效率而是为了让放行逻辑一目了然。你要让哪个接口免登录就明明白白写在这里后面查阅也方便。响应式写法注意这里用的是ServerWebExchange和MonoVoid这是WebFlux的模型。如果你习惯了Spring MVC的HttpServletRequest在这里会很不适应但必须用响应式写法才是网关正确的姿势。Controller层那套ModelAndView在WebFlux里不存在。用户信息传递校验通过后我们把userId和username重新放进了请求头。这样做的原因是网关与业务服务之间是HTTP转发服务端拿不到“网关内存里的用户对象”唯一可靠的信息携带方式就是把数据写进请求头。下游服务只需要约定好读X-User-Id这个header就能拿到当前用户。getOrder()的作用返回值越小优先级越高。这里设置成-100是为了让登录校验过滤器排在绝大多数内置过滤器之前执行尤其是排在NettyRoutingFilter负责转发之前。如果不设置Order默认按类名字典序排很可能转发请求的过滤器先执行了你的校验逻辑就没意义了。3.3 JwtUtil简化实现上面用到的JwtUtil不是本系列的重点但为了让你能跑起来给一个最简化的版本基于jjwtComponent public class JwtUtil { private SecretKey key Keys.hmacShaKeyFor(your-256-bit-secret-your-256-bit-secret.getBytes()); public String createToken(Integer userId, String username) { return Jwts.builder() .claim(userId, userId) .claim(username, username) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() 1000 * 60 * 60 * 2)) .signWith(key) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload(); } }生产环境里密钥一定不要硬编码配置到配置中心并定期轮换这是基本要求。JWT是无状态的签发之后无法主动作废所以如果系统里有“强制下线”“修改密码后踢掉旧token”这类需求JWT需要配合黑名单或session存储使用这点后面单独开一篇细讲这里先标记一下。4. GatewayFilter服务级局部过滤器的写法与适用场景GlobalFilter解决了“所有请求统一校验”的问题但实际项目里还有一种需求某个服务、某条路由有特殊处理逻辑其他服务不需要。如果把这种逻辑也塞进GlobalFilter全局过滤器就变成一个堆满if else的大杂烩。这时候就需要GatewayFilter出场。4.1 GatewayFilter和GlobalFilter的分工边界我用一张对比表讲清楚两者差异对比项GlobalFilterGatewayFilter生效范围所有经过网关的请求仅绑定到特定路由的请求声明方式实现GlobalFilter接口并注册为Bean通过过滤器工厂或GatewayFilter实例配置方式无需路由配置自动参与链路在route的filters节点中显式声明典型场景登录校验、跨域、全局日志、限流特定服务签名校验、特定路径重试、响应头修改是否可复用全局复用按路由复用一个工厂可建出多个实例通俗一点理解GlobalFilter是小区大门的保安所有车辆进入都要查GatewayFilter是某栋楼的门禁只有去这栋楼才需要刷卡。登录校验这种“全小区统一标准”的事用GlobalFilter某个管理后台需要额外验一次内部签名用GatewayFilter。4.2 写一个带参数的GatewayFilterFactoryGatewayFilter与GlobalFilter的写法不同。你可以直接new一个GatewayFilter匿名对象但在路由配置里享受不到配置参数的便利。更规范的做法是写一个过滤器工厂继承AbstractGatewayFilterFactory然后把可配置项放到内部类里Component public class SignatureGatewayFilterFactory extends AbstractGatewayFilterFactorySignatureGatewayFilterFactory.Config { public SignatureGatewayFilterFactory() { super(Config.class); } Override public GatewayFilter apply(Config config) { return (exchange, chain) - { // 仅当配置的checkSignature为true时执行 if (config.isCheckSignature()) { String signature exchange.getRequest().getHeaders().getFirst(X-Signature); if (signature null || !verifySignature(signature, exchange)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); DataBuffer buffer exchange.getResponse().bufferFactory() .wrap({\code\:401,\message\:\非法签名\}.getBytes(StandardCharsets.UTF_8)); return exchange.getResponse().writeWith(Mono.just(buffer)); } } return chain.filter(exchange); }; } private boolean verifySignature(String signature, ServerWebExchange exchange) { // 这里写你的验签逻辑比如MD5、HMAC或者调用密钥服务 return true; } public static class Config { private boolean checkSignature; // getter/setter public boolean isCheckSignature() { return checkSignature; } public void setCheckSignature(boolean checkSignature) { this.checkSignature checkSignature; } } }然后在路由配置里用spring: cloud: gateway: routes: - id: admin-service uri: lb://admin-service predicates: - Path/api/admin/** filters: - StripPrefix1 - name: Signature args: checkSignature: true配置里name就是工厂类名的前缀去掉FactorySignatureGatewayFilterFactory对应Signature。Spring Cloud Gateway启动时会扫描容器里的GatewayFilterFactory Bean然后按这个命名规则把配置和工厂匹配起来。4.3 过滤器工厂常见的坑Shortcut和Fully Qualified写法路由配置里过滤器有两种写法这个细节容易让人困惑。一个是shortcut写法比如StripPrefix1适合单参数场景另一个是fully qualified写法比如上面用到的name args方式适合带多个参数的场景。如果你的Config里有多个字段shortcut写法的参数顺序要跟Config字段名保持一致否则取到的值会错位。我自己写多参数工厂时一律用fully qualified写法代码和配置都可读性更高也不会出现参数顺序错误这种很难排查的问题。5. 登录校验的完整链路串讲从请求进来到下游拿到用户身份代码写完以后一定要把整条链路在脑子里过一遍因为它涉及的组件多新手往往会把“网关校验”和“业务服务拿用户”这两个环节搞混。这里我用一个具体请求来串讲。5.1 完整请求时序假设用户要调用GET /api/order/myorders查询自己的订单列表。链路是这样的客户端带上Authorization: Bearer eyJhbGciOi...请求头访问网关。GatewayHandlerMapping解析路径匹配到order-service这条路由。过滤器链组装完成AuthGlobalFilter的filter方法被调用因为Order值是-100它排在最前面执行。过滤器取出token调用JwtUtil解析校验签名和过期时间。校验通过后请求头被写入X-User-Id: 10086和X-User-Name然后chain.filter继续。LoadBalancerClientFilter通过lb://order-service找到实例NettyRoutingFilter把请求转发出去。order-service收到请求从X-User-Id头里读出userId10086执行select * from orders where user_id 10086。如果token解析失败或缺失AuthGlobalFilter写回一个401 JSON响应请求到此为止根本不会触达order-service。你在实际联调的时候可以分别在网关过滤器和下游服务的拦截器里打印日志观察这个顺序。我见过不少同事以为网关转发时会把token也带给下游然后下游再解析一遍token——其实完全没有必要。token的作用在网关已经结束了下游直接信任网关写入的header即可。5.2 为什么用X-User-Id这类请求头传递用户信息你可能想问为什么不把原始token传到下游让服务自己解析原因有两个。第一业务服务如果引入token解析逻辑那“登录校验统一在网关”这个架构约定就被破坏了等于你在每个服务里又埋了一份认证代码。第二网关在下游面前代表的是“已经被认证过的请求”下游不需要知道token长什么样只需要知道这个人是谁。用一个自定义请求头传递用户标识是网关与服务之间的内部约定和外部请求携带的Authorization头是两个维度的东西。这里需要提醒一下安全细节网关到服务之间的调用如果网络环境不是完全可信最好做一层内网授权避免外部请求绕过网关直连业务服务端口。我经历过的生产事故里Nacos暴露端口、业务服务端口直接挂在公网上导致的越权问题占了不小比例。网关校验做得再完善如果业务服务可以绕开网关被直接访问一切都是白搭。最稳妥的做法是业务服务只在内网监听把对外暴露的端口收敛到网关一台机器上。5.3 白名单动态化的进阶思路前面的代码里白名单是硬编码的List这在快速落地时没什么问题但运营一段时间后你会发现需求一直在变临时开放某个活动页、紧急下线某个老接口、根据环境放行Mock接口。与其每次改代码发版不如把白名单放到Nacos配置中心过滤器里每次动态读取。用Spring Cloud Alibaba的RefreshScope可以做到配置刷新时自动更新Component RefreshScope public class WhiteListConfig { Value(${auth.white-list:}) private String whiteListStr; public ListString getWhiteList() { if (StringUtils.isBlank(whiteListStr)) { return Collections.emptyList(); } return Arrays.asList(whiteListStr.split(,)); } }配置文件里对应auth: white-list: /api/auth/login,/api/auth/register,/api/auth/captcha然后过滤器里把硬编码的WHITE_LIST换成调用whiteListConfig.getWhiteList()即可。优势很明显运营要放行一个接口配置中心加一行配置刷新后立刻生效不用重启网关。而且配置中心的修改日志本身就是一个审计记录比翻代码提交记录方便得多。6. 过滤器实战中躲不过的坑与排错思路代码能跑通只是开始网关过滤器这个位置特殊一旦出问题影响的是所有经过网关的请求。我把自己实际踩过的几个典型坑列出来附带完整排查链路你遇到类似问题可以直接对照。6.1 过滤器不生效检查Bean注册和路由匹配现象写了AuthGlobalFilter启动后请求照样能打到业务服务完全不经过校验。排查链路先确认过滤器类是否被Spring扫描。没加Component或者类不在主启动类能扫到的包路径下过滤器根本不会注册。再确认请求路径是否匹配了路由规则。如果路径没匹配任何路由请求返回404不会走到过滤器链如果匹配了路由但断言写错了请求会匹配到另一条路由看起来像是过滤器“没生效”。最后看一个容易忽略的点你是否在网关模块里引入了spring-boot-starter-web。一旦引入网关会尝试用Spring MVC的方式处理请求路由匹配机制可能失效表现就是过滤器链压根不执行。我遇到过一次很隐蔽的情况过滤器类加了Component包路径也对但网关是多个application共享一个公共包的过滤器类被其他服务加载了网关这边反而因为Bean冲突没注册上。所以我的建议是网关模块的过滤器代码独立放在gateway-server自己的包路径下别放在一个所有人都依赖的common包里。6.2 Order排错白名单接口为什么也被拦截住现象登录接口已经放在了白名单里但请求时仍然提示“缺少有效的访问凭证”。排查链路先确定过滤器执行顺序是否和你预期一致。如果你又写了一个请求日志过滤器Order0或默认顺序它先执行了日志记录再执行AuthGlobalFilter日志可能没记录到什么但如果有一个缓存过滤器Order-200先读取了请求体并把请求体的流消费掉了后续过滤器读不到body就会误判为无token。检查路径匹配是否精确。/api/auth/login和/api/auth/login/在URI里是不同的后者携带了结尾斜杠精确匹配会失败。要么在配置端统一不带斜杠要么白名单判断时用endsWith加stripTrailingSlash处理。检查是否有其他过滤器改写了路径。比如StripPrefix虽然写在路由配置里但它的执行时机相对靠后。白名单判断在路径改写之前所以/api/auth/login能匹配上如果某个过滤器提前重写了路径白名单判断就失效了。这类问题说白了就是过滤器执行顺序引发的连锁反应。排查时最直接的手段是在每个过滤器里打日志打印当前路径、Order值和关键请求头就能快速定位在哪一环被拦了。6.3 CORS跨域配置放行不了现象前端请求带上了Origin头网关设置了全局跨域但浏览器仍然报跨域错误。排查链路Spring Cloud Gateway的跨域配置路径是spring.cloud.gateway.globalcors.cors-configurations和Spring MVC的cors.allowed-origins配置不在一个地方。如果写到了Spring MVC的配置里网关根本不会生效。确认是否配置了allowed-origin-patterns而不是allowed-origins。如果你需要支持携带Cookie的跨域请求allowedOrigins不能用*但allowedOriginPatterns可以配合allowCredentialstrue使用。还要注意如果你在AuthGlobalFilter里返回401时设置了Content-Type: application/json但没有保留跨域响应头浏览器一样会报错。建议统一在CORS配置里把allowedHeaders设为*并显式暴露X-User-Id这类自定义头否则下游服务即使收到了用户信息前端也读不到相关响应头。我在网关里配跨域时踩过最无语的一个坑是前端请求先触发了OPTIONS预检网关默认情况下对OPTIONS请求也会走AuthGlobalFilter结果预检没有带Authorization头直接被拦成401前端拿到401还不认为是跨域错误排查半天。处理办法是CORS预检请求直接放行if (HttpMethod.OPTIONS.equals(exchange.getRequest().getMethod())) { return chain.filter(exchange); }这个判断放在白名单检查之前因为预检请求根本没有业务token。6.4 统一响应格式客户端无法解析现象登录校验失败后前端说拿到的返回体不是JSON。排查链路看我代码里响应设置exchange.getResponse().setContentType(MediaType.APPLICATION_JSON)。如果你忽略这一步默认响应头可能是text/plain前端用response.json()解析就会抛异常。注意字符串拼接的JSON里有中文直接getBytes()默认编码可能出乱码最好指定StandardCharsets.UTF_8。我上面的代码里已经做了这个处理。响应体结构要统一。网关返回的JSON格式尽量和业务服务返回的错误体格式保持一致比如都有code、message字段。不然前端同一个接口登录失效时返回一种结构业务异常时返回另一种结构处理起来就要写两套逻辑。6.5 读取请求Body的过滤器要小心流不可重复读如果你要做一个验签过滤器需要读取请求体内容参与签名计算这就会遇到WebFlux下一个经典问题请求Body的DataBuffer流只能读一次。你读完之后后面的过滤器再读就拿到空值甚至转发的请求体都会丢。我的处理方案是把Body内容缓存到exchange的attribute里就缓存在当前请求上下文的一个属性中后续需要的人从attribute里拿。大致的思路是public static final String CACHED_BODY cachedBody; public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { return DataBufferUtils.join(exchange.getRequest().getBody()) .flatMap(dataBuffer - { byte[] bytes new byte[dataBuffer.readableByteCount()]; dataBuffer.read(bytes); DataBufferUtils.release(dataBuffer); exchange.getAttributes().put(CACHED_BODY, new String(bytes, StandardCharsets.UTF_8)); return chain.filter(exchange); }); }然后在转发给下游之前用缓存的body重新构造一个Request。这段逻辑写起来略繁琐但它解决的是“网关层做验签”必须面对的流问题。如果业务场景不需要读body就尽量别读避免引入这种复杂度。最后的几句实在话把登录校验收敛到网关以后最大的感受是各业务服务干净了新增服务再也不用复制一遍认证代码新来的同事也不需要理解“我们服务里这段token解析是干嘛的”。架构上的收益不是体现在某一个接口变快了而是整个系统的认证逻辑有了唯一的回答路径“请求是谁校验的网关。请求里的用户怎么来的网关写进去的。”这里再分享一个小技巧网关过滤器虽然功能强大但请始终保持无状态。不要在过滤器里引入本地缓存、定时任务或者复杂的状态同步网关是无状态节点才能水平扩容。校验所用的白名单、密钥、黑名单等数据宁可每次从配置中心或远程服务读取也不要图快放进本地内存——除非你能接受多实例网关之间数据不一致带来的诡异问题。过滤器的执行顺序也在迭代中越来越讲究。白名单先行、验签次之、登录校验随后、改写请求头最后这基本是网关过滤器的默认布局。后面如果要加限流、灰度路由插在合适的位置上就好但顺序和优先级一定要显式定义不要指望默认排序能帮你。下一篇准备写网关的限流和灰度发布这两个话题在实际生产里比登录校验更考验对网关原理的理解到时候继续聊。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →