ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微服务网关登录校验实战:Spring Cloud Gateway过滤器选型与避坑

微服务网关登录校验实战:Spring Cloud Gateway过滤器选型与避坑 在微服务架构里摸爬滚打到第14篇终于把手伸到了最操心的一层——网关。前面几篇我们把服务注册、配置中心、负载均衡、熔断限流都过了一遍服务之间的通信已经非常顺滑了。但凡是做企业级项目始终有个绕不开的坎登录状态到底在哪校验是每个服务各自写一次拦截器还是统一定点处理答案说出来大家可能都知道网关。但真正动手的时候过滤器体系、JWT解析、白名单放行、响应处理、跨域顺序每一样都能把人折腾得够呛。这篇文章就围绕Spring Cloud Gateway的登录校验做一次完整拆解重点讲清GlobalFilter和GatewayFilter在实际项目中怎么选、怎么写、怎么避坑。适合正在接触SpringCloud、想理解网关过滤器机制或者已经在网关层遇到校验问题的同学参考。内容不空谈理论全部是我在真实业务场景里跑通的方案包含可复制的代码和配置以及排了几个通宵才摸清楚的坑。1. 登录校验为什么一定要交给网关1.1 如果每个服务各自校验会出现什么事的坑先聊一个很现实的问题没有网关统一做登录校验的时候系统到底有多难受。刚开始做微服务的时候项目里只有两三个服务登录校验写在每个服务自己的拦截器里倒也没觉得什么。但服务一多起来问题立刻暴露了。最明显的就是重复代码用户信息解析、Token有效期判断、未登录异常处理每个服务都要写一套而且写法还能写得千奇百怪。有的团队把解析逻辑封装成公共starter结果版本一升级所有服务都要跟着发版光是协调发布时间就能让人血压升高。更要命的是安全隐患。因为校验逻辑分散在各服务可能会漏掉某一两个内部服务——认为反正是内网调用不会被外部直接访问。但微服务架构下的网关本身就是一个入口如果哪个服务没加校验逻辑或者配置漏了接口就直接裸奔在公网上了。我记得有一次排查线上问题发现某个报表服务完全没走鉴权逻辑Postman直接调一下地址就能拉到全量数据。发现问题的那天晚上我深刻意识到登录校验这种事只适合放网关这种必经之路上统一处理而不是靠大家自觉去各自的过滤器里写一遍。另外还有审计与链路的问题。登录校验放在各服务里每个服务记录的用户信息格式不统一有的记UUID有的记用户名有的记手机号排查一个跨服务请求的问题时日志互相之间根本对不上号。所有请求都经过网关的话用户身份在入口处统一解析、统一注入到Header下游服务拿到的都是同一套用户标识和上下文排查链路、做操作审计都要轻松得多。1.2 网关层校验到底解决了哪些核心问题放到网关做登录校验本质上是把身份识别这件事从各业务服务里抽出来收敛到一个统一的横切点上。这样做有几个非常直接的好处。统一入口降低遗漏风险。所有外部请求无论如何都要先经过网关那么校验只写一次就不存在哪个服务忘加过滤器导致接口裸奔的情况。只要网关把住了后面的服务默认都是可信的内网调用。统一上下文方便下游服务。网关解析完Token之后可以把用户ID、用户名、角色等关键信息放到请求头里再转发给下游服务。下游服务的代码里不需要关心Token怎么解析、有效期怎么判断直接从Header里取值UseCase即可。这样也顺带解决了服务间调用时用户信息丢失的问题。统一失效与刷新策略。登录状态踢人、黑名单、Token自动续期这类逻辑在网关集中处理最方便。比如用户被管理员封禁网关在确认Token合法后还要检查一下Redis黑名单如果命中就直接拦截。这种状态判断如果分散在每个服务里做数据一致性很难保证网关集中做才算靠谱。不过也要划清边界网关做的是登录校验和基础权限校验不负责复杂业务鉴权。比如这个订单是不是当前用户创建的这种数据权限一定得放在业务服务里做。如果什么都往网关塞Gateway会越来越臃肿反过来拖累整个系统的性能。1.3 网关校验的适用边界与设计原则根据我在生产环境踩坑之后的总结网关过滤器的职责应该控制在三件事以内一是请求是否已登录Token合法且未过期二是用户角色是否达到接口要求的基础准入可选三是必要的请求上下文注入用户ID、内部标识等。至于更细的权限点、菜单权限、按钮级别权限、数据权限都建议下放到业务服务。这种分法不是拍脑袋定的。网关层做的校验通常是通用性的粗粒度判断但它没法感知业务上下文比如订单系统想判断一个用户能否操作某条工单必须有业务数据参与计算网关这边的请求头里根本不够用。强行在网关里做就得让网关去查业务库这会把网关联调得越来越重网关最擅长的快速转发优势就没了。我见过有些项目把权限点全部维护到网关用数据库动态配置权限开关结果网关启动要连业务库每次请求要把URL、Method、角色、权限点全部比对一遍接口平均延迟增加了20ms以上。所以原则很简单网关做身份业务做权限。能把这两个概念分清楚整个系统的可维护性会明显提升。2. 过滤器体系选型GlobalFilter 和 GatewayFilter 到底差在哪2.1 先搞懂 Gateway 里两套过滤器机制Spring Cloud Gateway的过滤器体系分为两类官方文档里写得很清楚一个是GatewayFilter一个是GlobalFilter。名字相似但定位完全不同。GatewayFilter是局部过滤器需要通过路由配置里的filters参数指定只对绑定到该路由的请求生效。GlobalFilter则是全局过滤器不跟具体路由绑定只要请求进入网关都会过一遍。实际工程里这两者经常会被人混淆。尤其是刚接触网关的同事看到GlobalFilter带了个Global就想当然认为它更高级做什么都往GlobalFilter里写。但GatewayFilter并不是没用的FilterSpring Cloud Gateway自身实现路由转发就用了一堆内置的GatewayFilter工厂比如AddRequestHeader、StripPrefix、RewritePath、TokenRelay这些都是在路由层面做精细化控制的灵活度非常高。从代码结构上看两者都实现了过滤器的核心逻辑接口甚至GlobalFilter与GatewayFilter之间也有兼容关系——Gateway内部会通过GatewayFilterAdapter把GlobalFilter包装成GatewayFilter让它们共用同一套执行链。也就是说真正执行的时候它们都在同一条过滤器链上区别主要体现在如何被触发以及如何被配置。2.2 内置 GatewayFilter 工厂带来的便利GatewayFilter最值得称道的一点是它有很多官方内置的工厂不用写一行Java代码就能实现不少通用逻辑。比如AddRequestHeader可以在转发前给请求加上自定义HeaderRemoveRequestHeader可以剥离敏感HeaderRetry可以做请求重试RequestRateLimiter可以做网关层限流。举个例子前端调用的接口路径通常带版本号比如/api/v1/order/list但后端服务里定义的Controller路径是/order/list此时可以用StripPrefix1把路径里的第一个前缀去掉再转发给下游。类似这种诉求用内置网关过滤器配置几行YAML就行根本不用写自定义过滤器。这就是GatewayFilter在局部精细化控制上的价值。比如某个特殊路由需要额外的Header认证而其他路由不需要某些开放接口如获取验证码不需要登录校验但要加一个请求频率限制。这些差异化的逻辑用GatewayFilter来挂比用GlobalFilter里写大量if-else清晰得多。2.3 全局与局部的核心差异对比为了看起来更直观我整理了一份对比。日常面试里如果被问到网关这块这个表格基本就能把两套过滤器的区别讲透维度GatewayFilterGlobalFilter作用范围绑定到某个路由只对匹配该路由的请求生效全局生效所有经过网关的请求都会进入配置方式在spring.cloud.gateway.routes.filters里配置可复用内置工厂通过Component注册成Spring Bean自动生效是否可复用官方内置了大量可直接配置的工厂也可通过实现接口自定义需要自行实现全局统一逻辑适合放这里执行顺序在路由级Filter链中按配置顺序执行通过getOrder()方法控制执行顺序影响所有请求典型场景路径重写、加Header、局部限流、局部Token校验全局登录校验、统一日志TraceId注入、统一跨域处理从这张表能看出GlobalFilter适合放横切逻辑比如登录校验这种任何请求都躲不过去的事GatewayFilter适合放定向逻辑比如只针对某个路由生效的处理规则。把它们混着用也没问题但脑子里一定要清楚一个请求走网关时Gateway会先把匹配到的路由下的GatewayFilter集合起来再把全局的GlobalFilter适配成GatewayFilter最终按order值排好序执行。谁先谁后看的是order不是定义顺序。2.4 登录校验为什么选择 GlobalFilter 而不是 GatewayFilter实现登录校验选GlobalFilter几乎是必然的。原因很简单——登录状态是所有业务请求都要校验的不存在只给部分路由做登录限制这种需求。如果我用GatewayFilter配置就得在每一条路由的filters下面都加上这个过滤器路由一多配置就变得又长又重复而且新增路由时一旦忘了加过滤器新接口就没有任何防护。这恰恰是网关层做统一校验最忌讳的事。用GlobalFilter还有一个好处就是可以统一控制执行顺序。我可以在登录过滤器里返回一个order值让它在CORS处理之后、路由转发之前执行。其他全局过滤器也可以通过order值来指定与登录校验的相对先后关系比如先TraceId再登录再限流编排起来非常灵活。当然如果某些路由确实想绕过登录校验比如登录接口本身、获取验证码接口、回调接口我不建议在GlobalFilter里写死一堆路径判断更推荐用白名单的方式在配置文件里维护。这一点在下一部分实现时会展开说明。3. 核心实现自定义 GlobalFilter 完成登录校验3.1 环境准备与依赖清单先把环境说清楚。我的这套方案基于Spring Cloud 2021.x版本Spring Boot用的2.6.xSpring Cloud Gateway本身是WebFlux体系所以不要引入spring-boot-starter-web否则依赖冲突能让人怀疑人生。服务注册与配置中心使用的Nacos登录认证用的是JWT具体为java-jwt库加Redis存储会话状态。核心的依赖如下如果你用的版本不同注意对齐Spring Cloud Alibaba的版本号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.auth0/groupId artifactIdjava-jwt/artifactId version3.19.2/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis-reactive/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId /dependency这里要注意一点在Gateway项目里操作Redis时最好引入spring-boot-starter-data-redis-reactive因为Gateway基于Netty用非阻塞的ReactiveRedisTemplate与请求链路更搭配。如果误引入了普通的spring-boot-starter-data-redis虽然也能用StringRedisTemplate但会出现阻塞调用在网关这种高并发入口处性能会很吃亏。Nacos作为注册中心的配置我这里就不重复贴了上一篇已经聊过。如果Gateway需要动态读取路由配置还需要引入spring-cloud-starter-alibaba-nacos-config并配置shared-configs或者extension-configs。我这里Nacos只负责服务发现路由配置还是放在本地YAML里先跑通免得把问题复杂化。3.2 分清塔台与飞机网关校验的完整请求链路写代码前先把流程想清楚。我习惯把Gateway比作机场安检口全局过滤器就是那台安检机。请求到达网关后不会立刻转发到后面的业务服务而是先经过这一道安检第一步接收请求检查是否命中白名单比如登录接口、注册接口、静态资源路径如果命中直接放行不做过多的逻辑判断。 第二步从请求头里取出Token字段一般约定叫Authorization或者token。没取到就返回401提示未登录。 第三步解析并校验Token。这一步包含JWT签名校验、过期时间校验。如果Token是非法的直接返回401如果合法继续往下。 第四步从Redis里查一下当前用户会话是否存在防止Token本身合法但后端已经把用户踢下线了。 第五步把用户ID、用户名等关键信息写入请求头比如X-User-Id、X-User-Name再放行给下游服务。这五步看起来简单实际上每一步都有不少细节。比如白名单判断不能用简单的if (path.equals(/auth/login))因为Spring Cloud Gateway里拿到的是ServerHttpRequest路径可能带上下文前缀路径匹配需要做好处理。再比如放行后要记得调用chain.filter(exchange)否则请求就卡在过滤器里不走了。这两个都是新手最容易踩的点。3.3 核心代码自定义 GlobalFilter 登录校验过滤器下面直接给出我项目里沉淀下来的一套代码骨架。我删掉了一些业务无关的东西保留主干逻辑方便大家按需扩展。Component public class AuthGlobalFilter implements GlobalFilter, Ordered { private static final String[] WHITE_LIST { /auth/login, /auth/register, /auth/captcha }; Resource private ReactiveRedisTemplateString, String redisTemplate; Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getURI().getPath(); // 1. 白名单直接放行 for (String whitePath : WHITE_LIST) { if (path.startsWith(whitePath)) { return chain.filter(exchange); } } // 2. 获取Token String token request.getHeaders().getFirst(Authorization); if (StringUtils.isBlank(token) || !token.startsWith(Bearer )) { return unauthorized(exchange, 未登录或Token缺失); } token token.replace(Bearer , ); // 3. 解析Token Long userId; String username; try { DecodedJWT jwt JWT.require(Algorithm.HMAC256(your-secret-key)) .build() .verify(token); userId jwt.getClaim(userId).asLong(); username jwt.getClaim(username).asString(); } catch (JWTVerificationException e) { return unauthorized(exchange, Token无效或已过期); } // 4. 校验Redis会话是否存在踢人下线场景 String sessionKey login:token: userId; return redisTemplate.hasKey(sessionKey) .flatMap(exists - { if (Boolean.FALSE.equals(exists)) { return unauthorized(exchange, 会话已失效请重新登录); } // 5. 写入用户信息放行 ServerHttpRequest mutatedRequest request.mutate() .header(X-User-Id, String.valueOf(userId)) .header(X-User-Name, username) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); }) .then(); } private MonoVoid unauthorized(ServerWebExchange exchange, String msg) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON); byte[] bytes ({\code\:401,\message\:\ msg \}).getBytes(StandardCharsets.UTF_8); DataBuffer buffer exchange.getResponse().bufferFactory().wrap(bytes); return exchange.getResponse().writeWith(Mono.just(buffer)); } Override public int getOrder() { return -100; } }这里的getOrder()返回-100是为了让登录过滤器在大多数过滤器之前执行。Spring Cloud Gateway里order值越小越先执行。负数区间可以让它排在各种默认filter前面。有一个细节我得特别说下代码里redisTemplate.hasKey(sessionKey)返回的是MonoBoolean所以后续逻辑必须放在flatMap里写。如果你习惯写同步代码很容易写成if (redisTemplate.hasKey(...))这在WebFlux里是行不通的。这一点也是好多人从Servlet MVC转向WebFlux后最大的不适应点。3.4 402. 配置网关路由与白名单管理配置文件这边除了常规的Nacos地址主要就是spring.cloud.gateway.routes的配置。做登录校验时路由配置本身其实不怎么影响过滤器的执行因为GlobalFilter对所有请求都生效。但路由配置决定了请求最终转发到哪个服务需要正确配置。下面贴一份简单的配置示例server: port: 8080 spring: application: name: gateway-server cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: auth-service uri: lb://auth-service predicates: - Path/auth/** - id: order-service uri: lb://order-service predicates: - Path/order/** filters: - StripPrefix1这里lb://表示从Nacos注册中心按服务名负载均衡调用。注意如果下游服务的Controller路径定义了完整前缀比如order服务里的请求路径本身就是/order/list那么网关这里就不需要StripPrefix直接转发即可。我在项目中就是网关接收/api/order/list转发到order服务时去掉/api所以在路由里配了StripPrefix1。白名单路径我个人不推荐硬编码在Java代码里。虽然上面的示例代码是写死数组但实际项目中最好从Nacos配置中心动态读取。比如放在Nacos配置里gateway: auth: white-list: - /auth/login - /auth/register - /auth/captcha - /actuator/health配置类里用ConfigurationProperties(prefix gateway.auth)绑定一下再交给过滤器使用即可。这样上线时调整白名单不用改代码、不用重启网关等Nacos配置中心的自动刷新生效就够了。还有一个非常关键的跨域配置。网关层面配跨域不要只配spring.cloud.gateway.globalcors就万事大吉因为GlobalFilter的执行顺序和跨域处理器的顺序一旦不对前端浏览器的预检请求OPTIONS会被登录过滤器拦下来导致前端控制台报CORS错误同时又说未登录。我的做法是配置globalcors之后在日志过滤器里确认预检请求直接放行if (HttpMethod.OPTIONS.equals(request.getMethod())) { return chain.filter(exchange); }这种处理方式在前后端分离的项目里几乎必踩后面排查问题那块我再详细说明。3.5 自定义 GatewayFilter更适合局部场景的过滤器除了GlobalFilter可以再补一个GatewayFilter的自定义示例这样两套过滤器大家都能看到真实写法。局部过滤器的典型使用场景是给特定路由加自定义Header或者记录这个路由的耗时。实现方式很简单实现GatewayFilter接口然后在配置类里用Bean返回或者在路由配置里直接引用。下面这段代码给指定路由添加了一个自定义Headerpublic class CustomHeaderGatewayFilter implements GatewayFilter { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest().mutate() .header(X-Custom-Source, gateway) .build(); return chain.filter(exchange.mutate().request(request).build()); } public static class Config { private String name; private String value; // getter/setter } } Component public class CustomHeaderGatewayFilterFactory extends AbstractGatewayFilterFactoryCustomHeaderGatewayFilter.Config { public CustomHeaderGatewayFilterFactory() { super(Config.class); } Override public GatewayFilter apply(Config config) { return (exchange, chain) - { ServerHttpRequest request exchange.getRequest().mutate() .header(config.getName(), config.getValue()) .build(); return chain.filter(exchange.mutate().request(request).build()); }; } }有了GatewayFilterFactory路由配置里就可以这样使用spring: cloud: gateway: routes: - id: demo-service uri: lb://demo-service predicates: - Path/demo/** filters: - CustomHeadername, value这种写法对于那些只对特定路由生效的小功能非常合适。比如你只想给某个第三方对接接口单独加一个签名Header其他路由不要那就用GatewayFilter不用动GlobalFilter的逻辑。4. 实际项目里的坑点与排查经验4.1 过滤器取不到 PathVariable 的问题GlobalFilter里想直接拿路由模板里的变量比如/order/{orderId}中的orderId是不行的。因为GlobalFilter作用于所有请求它根本不知道当前请求匹配的是哪条路由模板所以拿不到orderId这种参数。要想拿可以通过exchange.getAttribute(ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE)而且必须是在路由匹配完成之后才能拿到。但这里有个坑GET请求往往路径中直接带有orderId而POST请求通常把它放在Body里网关默认不会读取Body因为Body一旦读取后如果不缓存下游服务就收不到请求体了。所以如果是POST请求参数校验建议别指望在GlobalFilter里拿业务参数老老实实只做Header和Token层面的校验。我的一个经验是如果实在需要路径参数做某些校验比如管理员重置某个订单时需要判断订单归属可以在路由匹配之后的过滤器里获取但务必要记住只能在ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE存在的情况下使用并且拿到后立刻写入Header或Attribute不要在下游再重复解析。4.2 响应体中文乱码与 JSON 格式统一如果按照最早我给的unauthorized方法实现直接写入字节数据其实是能处理中文的因为使用了StandardCharsets.UTF_8。但很多人会忽略一件事返回的Content-Type要带charset。仅仅设置MediaType.APPLICATION_JSON在某些HTTP客户端里可能默认按ISO-8859-1解码导致中文变成乱码。稳妥的做法是设置请求头的Content-Type为application/json;charsetUTF-8exchange.getResponse().getHeaders().setContentType( MediaType.APPLICATION_JSON );如果用的是MediaType.APPLICATION_JSON它不带charset但Netty通常也能正确处理UTF-8因为MediaType.APPLICATION_JSON默认的字符集就是UTF-8。不过为了保险起见更推荐使用MediaType.APPLICATION_JSON_VALUE ;charsetUTF-8或者MediaType.parseMediaType(application/json;charsetUTF-8)的方式。另外一个问题是统一响应结构。不同服务返回的未登录格式可能不一样有的是{code:401}有的是{status:401}还有的直接返回字符串。因此网关注入的401响应一定要统一格式并在网关处拦截所有异常把非200的响应转换为统一的JSON结构。如果团队里已经有公共的响应体Result类网关这里最好单独定义一份简化的避免网关层引用业务服务里的公共类导致耦合。4.3 Redis 连接串配置与踢人下线网关校验用Redis配置上要注意reactive相关属性。常规的spring.redis.host配置在现代版本里已经迁移到spring.data.redis.host但WebFlux项目里很多老旧的写法仍然兼容。我遇到过一种情况本地Redis连得好好的部署到测试环境就报Connection refused排了半天才发现测试环境的Redis没有配置密码而网关连了一个带密码的Redis地址导致认证失败。排查的时候容易误认为是网络问题其实看日志就能发现ERR AUTH字样。会话失效策略方面登录的时候业务服务往Redis写入login:token:{userId}并设置TTL为24小时网关层只负责读取判断。但如果用户频繁操作Token的剩余过期时间不会自动续期到点依旧会被踢下线。如果产品经理要求活跃用户不掉线就需要在网关里每次校验时把TTL刷新到24小时。这个操作在ReactiveRedisTemplate里需要单独调用expire要注意别在每次请求上都阻塞等待结果否则高并发下网关卡死就是自找的。4.4 懒加载与循环依赖的处理Spring Cloud Gateway项目里使用Autowired注入其他服务时偶尔会遇到循环依赖问题尤其在过滤器里使用Lazy注入比较重的Bean时更容易踩坑。我遇到过的情况是这样某次在GlobalFilter里注入了业务模块的Service因为历史原因把登录逻辑放在业务服务里启动时直接报循环依赖。解决方案有两种。一种是调整类之间的依赖关系把鉴权逻辑完全放在Gateway模块内部不依赖业务模块的Service。另一种是使用Lazy延迟注入不让Spring在启动阶段就初始化这对循环依赖public AuthGlobalFilter(Lazy SomeAuthService authService) { this.authService authService; }不过我个人不推荐在网关里注入业务Service网关要保持轻量。如果需要查用户状态或权限通过Redis或远程调用方式做尽量避免把一套ORM和Service都塞进网关。这个看架构设计如果你的团队坚持让网关直接查库就做好缓存与性能压测否则接口一有波动网关和数据库都会跟着遭殃。4.5 过滤器执行顺序的一张大坑CORS跨域配置与GlobalFilter的order值搭配问题属于高频问题里最隐蔽的一种。Spring Cloud Gateway在响应预检请求时如果CorsWebFilter的执行顺序晚于登录过滤器那么OPTIONS请求还没走到CORS处理就被登录过滤器拦截了。前端看到的报错不是未登录而是CORS policy: No Access-Control-Allow-Origin header排查起来很容易怀疑人生。我的做法是两条腿走路用spring.cloud.gateway.globalcors.cors-configurations配置跨域规则在登录过滤器里显式放行OPTIONS请求。也可以同时保证CorsWebFilter的order值足够小让CORS先执行。比如注册一个CorsWebFilter并设置setOrder(-200)Bean public CorsWebFilter corsWebFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsWebFilter(source); }这里有个特别容易混淆的细节。如果你设置了allowCredentials(true)AllowedOrigin就不能写成*必须用AllowedOriginPattern(*)或者指定具体域名。很多初学者在这一步深受其害浏览器里明明能看到响应头有Access-Control-Allow-Origin: http://localhost:5173但Request的Origin却带着http://localhost:5173如果反射出来的Origin不是精确匹配400错误说来就来。4.6 常见问题速查表最后梳理一份排查表覆盖我这几年在网关层登录校验上遇到过的共性问题。代码层面遇到Bug时可以对着表快速定位问题现象可能原因排查思路所有请求都返回401白名单没生效或Token解析异常先确认白名单路径匹配逻辑是否精确匹配再看JWT密钥和签发密钥是否一致只有某个服务请求401该服务路由被StripPrefix误裁剪检查路由配置中StripPrefix数量以及下游服务实际路径前缀CORS报错但没有响应头OPTIONS被全局过滤器拦截确认登录过滤器是否放行OPTIONS请求CorsWebFilter的order值Redis连接正常但登录一直失败Token校验与Redis key不一致对比登录服务写入Redis的key格式与网关读取的key格式注意userId类型差异下游服务拿不到用户信息请求头写入失败或下游读取Header名不一致统一Header命名规范比如都叫X-User-Id避免大小写和下划线混用网关内存飙升每请求都实例化大量对象或Redis响应阻塞检查是否存在阻塞调用比如用了同步RedisTemplate线程池被耗尽转发到服务后路径不对路由Predicate与StripPrefix配合错误分开理解Path谓词匹配的是网关入口路径转发时再根据下游实际路径调整5. 从登录校验扩展出去过滤器还能做什么登录校验只是GlobalFilter最常见的应用场景但过滤器能做的事情远不止于此。统一的TraceId链路追踪就是一个很好的扩展方向。网关在接收到请求时先判断请求头里有没有TraceId没有就生成一个UUID放到请求头中转发给下游再把TraceId写进MDC里这样网关自身的日志也能串联起来。下游服务只要接住这个Header整个调用链路的日志就都串起来了。这个功能完全可以放在登录校验GlobalFilter的前面作为一个独立的GlobalFilter实现。统一限流也可以放在网关层。Spring Cloud Gateway内置了RequestRateLimiter过滤器工厂配合Redis做令牌桶限流能挡住突发的流量冲击。我之前在一个秒杀场景中网关层配置了每秒单用户最多5次的限流规则效果很明显拦截下来的请求都不需要进入业务服务节省了大量数据库连接。灰度发布同样可以基于Filter来做。通过请求Header里的特殊标记把流量引导到不同版本的服务上。不过我建议灰度逻辑单独写一个GlobalFilter不要让登录过滤器去关心这些与身份认证无关的事情保持每个过滤器职责单一。6. 我个人的一点实战心得网关层的登录校验看起来只是微服务链路里的一小块拼图但真正落到生产环境需要考虑的点非常多。我在这个模块上反复改过好几轮最深的一点体会是过滤器的职责边界一定要控制得住。一开始我也想把用户权限点、按钮权限、数据权限全部收进网关后来被性能问题教训了之后才老老实实把身份和权限分开网关只做身份识别和会话校验。还有一点是日志。网关处在整个请求链路的入口接入Prometheus和日志平台之后过滤器的执行耗时、拦截次数、非法Token数量都值得做成指标。我后来加了一个简单的计数器统计每天被网关拦截的未登录请求数量这个数字能直接反映系统是否经常被爬虫扫、前端是否总是忘了带Token。如果你正在做Spring Cloud Gateway的登录校验建议先把这一篇里的代码跑通再根据自己项目的注册中心、配置中心和鉴权体系做替换。最重要的不是复制代码是理解过滤器的执行链路、order值的影响以及WebFlux这种响应式编程模型下链式调用的写法。把这几件事想明白网关层很多看起来诡异的问题其实都能一眼看出原因。
返回列表