资讯动态

Spring Cloud Gateway登录校验实战:自定义GlobalFilter+JWT+Redis统一微服务认证

发布时间:2026/10/3 2:53:06 来源:尧图企业网站定制
SpringCloud微服务项目做到网关这层几乎所有人都会撞上同一个需求登录校验不能在每个服务里重复堆。我目前在项目里就是用网关登录校验配合自定义GlobalFilter把认证逻辑沉淀到统一入口改造后效果立竿见影。这篇文章围绕SpringCloud Gateway里怎么做登录校验、怎么写自定义GlobalFilter、以及GateWayFilter和GlobalFilter到底什么区别来展开把我自己的落地方案和踩过的坑完整整理出来。这个内容适合正在做微服务拆分的团队参考也适合那些被各种概念绕晕、想直接看可运行代码的读者。我会从架构设计的为什么开始一路讲到过滤器源码级区别、JWTRedis的完整校验实现以及一套可以直接抄的排查清单。看完你不需要再去翻那些只讲概念的博客了。1. 网关登录校验为什么微服务一定要有这道统一防线1.1 微服务拆分的代价认证逻辑怎么从一份变成八份微服务架构图看起来很美拆订单、拆用户、拆商品各搞各的库各发各的接口。但很多人把服务拆完之后才发现一个尴尬问题——原来单体应用里写一次的用户登录校验现在每个服务都得写一遍。我之前带过的一个项目就是典型用户服务写了一份JWT解析订单服务自己又写了一份后来加了个支付服务又复制一份。等到token的加密密钥要更换时噩梦来了。你根本不知道哪些服务还在用旧密钥得全局搜索代码挨个服务发版。更离谱的是有个服务还把JWT解析代码改出了自己的风格别人一眼看不出是同一个东西。登录校验重复写的核心问题不只是代码冗余而是安全策略不一致。有的服务校验了用户状态有的只校验了签名有的干脆因为“内部服务”就不校验。网关登录校验解决的就是这个问题把认证收敛到一个点所有外部请求进系统只有一条路必须过这一道检查。1.2 网关校验的价值与边界统一入口带来的连锁收益网关做登录校验最直观的收益是入口统一。外部客户端根本不直接接触微服务它只知道一个网关地址。请求先到网关网关把token验完之后再转发给后面的订单服务、用户服务。这就是典型的“统一认证、向下透传”。第二个收益是防止绕过认证。只要下游服务不暴露公网端口所有流量都经过网关认证逻辑想跳过都难。生产环境中我建议把服务端口全部绑定内网管理面用独立的安全组控制网关是唯一对外的口子。第三个收益是省掉了每个服务的重复代码这个价值在团队协作时尤其明显。新来的同事接一个新服务不再需要搞明白token怎么验、Redis里用户信息怎么取他只需要读网关传过来的请求头就行。但也要说清楚边界。网关登录校验是入口防线不是全部安全体系。涉及支付下单、修改密码这类高危操作下游服务仍然需要做二次状态校验比如判断用户是否被禁用、权限是否足够。网关拦截的是“非法访问”下游校验的是“越权操作”。两者配合而不是互相替代。2. 动手前的准备依赖引入与路由配置2.1 项目依赖和版本选择的那些细节我用的是Spring Cloud Gateway而不是Zuul。原因不复杂Zuul 1.x基于Servlet同步阻塞模型Gateway基于WebFlux响应式非阻塞性能和生态都更好而且Spring Cloud官方后面的版本已经逐步放弃Zuul维护。如果你还在新项目里选Zuul我建议趁早换。先看pom.xml里的核心依赖。这里有一个特别容易踩的坑Gateway基于WebFlux一定不要引入spring-boot-starter-web否则启动直接报错或者出现奇怪的循环依赖。正确的依赖写法是这样dependencies !-- Spring Cloud Gateway 核心 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-gateway/artifactId /dependency !-- 注册中心我用的是 Nacos -- dependency groupIdcom.alibaba.cloud/groupId artifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactId /dependency !-- 负载均衡网关转发到服务必须要有 -- dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-loadbalancer/artifactId /dependency !-- JWT 工具库 -- dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency !-- Redis 客户端用于读取登录用户信息 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis-reactive/artifactId /dependency /dependencies注意Redis我引入的是reactive版本因为Gateway整个链路是响应式的如果你引入普通spring-boot-starter-data-redis想在过滤器里同步操作Redis会阻塞事件循环线程高并发下性能会很难看。用ReactiveRedisTemplate配合flatMap处理异步结果是网关场景下的正确姿势。2.2 路由配置里的两个关键开关路由配置相对简单但有两个配置细节直接影响后续的过滤器开发。先看一份可用的application.ymlserver: port: 8080 spring: application: name: gateway-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 gateway: routes: - id: user-service uri: lb://user-service predicates: - Path/user/** filters: - StripPrefix1 - id: order-service uri: lb://order-service predicates: - Path/order/** filters: - StripPrefix1 globalcors: add-to-simple-url-handler-mapping: true cors-configurations: [/**]: allowedOriginPatterns: * allowedMethods: - GET - POST - PUT - DELETE - OPTIONS allowedHeaders: * allowCredentials: true第一个关键点是lb://user-service它表示通过Nacos注册中心发现服务并走客户端负载均衡。直接写http://localhost:8081也行但那就丢失了注册中心动态感知的能力服务实例扩容缩容时网关感知不到生产环境不建议这么干。第二个关键点是StripPrefix1。客户端请求/user/login经过网关后Gateway会默认把完整路径转发给下游。如果下游服务的接口本身是/login多了个/user前缀就会404。StripPrefix1表示去掉路径中的第一段把/user/login变成/login再转发。这个配置很多人第一次接触会忽略结果路由通了但下游报404排查半天。2.3 网关的架构位置一个简单的链路图先在心里放一张微服务架构图不用画得很精细理解链路就够了客户端浏览器/App ↓ HTTP Nginx / SLB可选 ↓ Spring Cloud Gateway网关登录校验路由转发 ↓ AuthFilter自定义GlobalFilter ↓ 校验通过透传用户信息 用户服务 / 订单服务 / 商品服务网关在整个微服务体系里就像大楼的前台。所有人都要从前门进前台确认你身份没问题再放行到对应楼层。没有这个前台每个办公室都得自己装一套门禁管理起来成本翻倍安全策略还难以统一。3. GlobalFilter和GateWayFilter别再混淆这两个过滤器3.1 GlobalFilter的底层逻辑是“全局生效”先强烈建议你在看任何过滤器教程之前先去翻一下Gateway的源码结构。Spring Cloud Gateway里的过滤器分为两类GlobalFilter和GatewayFilter。从命名直译一个是全局过滤器一个是局部过滤器但真正理解它们的关系要深入一层。GlobalFilter是全局过滤器它会自动作用于所有路由不需要在路由配置里手动指定。它实现的接口长这样public interface GlobalFilter { MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain); }所有实现了GlobalFilter接口的Bean都会被Gateway自动装配到过滤链上。也就是说你只要写一个Component注解的过滤器类它就生效了不需要在每个路由下面写filters。全局登录校验、全局日志、全局限流这类横切逻辑用GlobalFilter最合适。3.2 GateWayFilter与GlobalFilter的协作方式GatewayFilter是局部过滤器它必须配置在某个路由的filters列表里才会生效比如StripPrefix、AddRequestHeader、Retry这些都是GatewayFilter的实现。如果你想给某个服务单独加一个响应头不用影响别的服务用GatewayFilter最合理。表格对比几个核心维度对比项GlobalFilterGatewayFilter生效范围所有路由仅配置了该filter的路由定义方式实现GlobalFilter接口实现GatewayFilter接口或使用内置工厂注册方式交给Spring容器即可配置在yml的routes.filters里典型使用登录校验、全局限流、日志单路由的StripPrefix、限流Order优先级必须实现Ordered接口或加Order注解按配置顺序生效这里要特别注意GlobalFilter和GatewayFilter最终会在同一个责任链里执行不是两套平行的链路。Gateway会把GlobalFilter包装成GatewayFilter再按Order排序统一执行。所以Order设计得不好局部过滤器可能跑在全局过滤器前面导致某些场景逻辑错乱。3.3 过滤器链的执行顺序与Order设计过滤器链执行顺序由Order决定数字越小越先执行。这里有一个反直觉的细节请求的“前置”阶段是Order小的先执行但“后置”阶段却反过来是Order大的后执行。因为在响应式编程里pre逻辑写在chain.filter(exchange)之前post逻辑写在之后整体执行像洋葱圈一样层层嵌套。我自己在网关里至少会定义三个Order级别的过滤器过滤器Order作用跨域处理过滤器-1000处理CORS最先执行登录校验过滤器-100解析并校验token日志过滤器-50记录访问日志跨域必须放在最前面否则浏览器发OPTIONS预检请求时会被登录校验直接拦截前端就会莫名其妙看到跨域报错。这个坑我踩过不止一次后面在常见问题里还会详细说。4. 登录校验核心实现自定义GlobalFilterJWTRedis4.1 校验链路设计网关登录校验不是单纯解析一个JWT就完事生产环境的校验链路通常要包含五步。我把这套流程用在两个项目里整体运行比较稳第一步白名单放行。像登录接口、注册接口、验证码接口这些本来就不需要token必须提前放行。白名单维护在Nacos配置中心里改配置不用重启网关这是生产环境的基础要求。第二步提取请求头。从Authorization中取出token如果为空直接返回401。第三步解析JWT。用密钥验签取出userId和username。这里要小心JWT过期和签名算法不一致两个问题后面我会单独展开。第四步查询Redis校验用户状态。JWT本身只能证明token没被篡改但证明不了用户当前是否有效。如果用户被管理员封禁或者密码被修改导致token必须失效光验JWT是拦不住的。所以网关需要拿着userId去Redis里查一下当前登录态看看用户状态是否正常。第五步透传用户信息。校验通过后把userId、username放进请求头转发给下游服务。下游服务不需要再解析JWT直接读请求头就可以识别当前用户。4.2 自定义GlobalFilter代码解析直接上一份我实际用过的代码骨架每个关键点都有注释说明Component Slf4j public class AuthGlobalFilter implements GlobalFilter, Ordered { Autowired private JwtProperties jwtProperties; Autowired private ReactiveRedisTemplateString, String redisTemplate; private static final String AUTHORIZATION_HEADER Authorization; private static final String BEARER_PREFIX Bearer ; // 白名单不需要登录即可访问的路径 private static final ListString WHITE_LIST Arrays.asList( /user/login, /user/register, /user/captcha ); Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getPath().value(); // 1. 白名单直接放行 if (WHITE_LIST.contains(path)) { return chain.filter(exchange); } // 2. 跨域预检请求直接放行 if (OPTIONS.equals(request.getMethod().name())) { return chain.filter(exchange); } // 3. 提取token String token resolveToken(request); if (StringUtils.isBlank(token)) { return unauthorizedResponse(exchange, 未携带Token请先登录); } // 4. 解析JWT Claims claims; try { claims Jwts.parserBuilder() .setSigningKey(Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8))) .build() .parseClaimsJws(token) .getBody(); } catch (ExpiredJwtException e) { return unauthorizedResponse(exchange, 登录已过期请重新登录); } catch (JwtException e) { return unauthorizedResponse(exchange, Token无效请重新登录); } String userId claims.get(userId, String.class); String username claims.get(username, String.class); // 5. 查Redis校验登录态 String redisKey login:token: userId; return redisTemplate.opsForValue().get(redisKey) .flatMap(tokenInRedis - { if (!token.equals(tokenInRedis)) { return unauthorizedResponse(exchange, 登录状态已失效); } // 6. 透传用户信息到下游服务 ServerHttpRequest mutatedRequest request.mutate() .header(X-User-Id, userId) .header(X-Username, username) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); }) .switchIfEmpty(unauthorizedResponse(exchange, 登录状态已失效)); } private String resolveToken(ServerHttpRequest request) { String header request.getHeaders().getFirst(AUTHORIZATION_HEADER); if (header ! null header.startsWith(BEARER_PREFIX)) { return header.substring(BEARER_PREFIX.length()); } return null; } private MonoVoid unauthorizedResponse(ServerWebExchange exchange, String message) { ServerHttpResponse response exchange.getResponse(); response.setStatusCode(HttpStatus.UNAUTHORIZED); response.getHeaders().add(Content-Type, application/json;charsetUTF-8); String body {\code\:401,\message\:\ message \}; DataBuffer buffer response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(buffer)); } Override public int getOrder() { return -100; } }这份代码有几个细节值得展开讲。首先是token解析用的Keys.hmacShaKeyFor这个API要求密钥长度至少32字节你用“123456”这种短密钥会直接抛异常。生产环境中密钥要通过配置中心下发不要写在代码里。其次是Redis的比对方式。这里是简单比较Redis里存的token和当前token是否一致。如果用户改密码、被踢下线、或者在别处重新登录Redis里的token应该被更新或被删除旧token就会失效。这样就实现了“用户主动退出后token立刻失效”的效果。还有switchIfEmpty这个响应式操作符。从Redis取不到值时会执行备用逻辑返回登录失效的响应。这里需要注意方法的返回类型统一用MonoVoid避免出现“看起来能编译但请求悬挂不返回”的问题。我之前就是因为漏掉了switchIfEmpty导致Redis里没有键时网关既不响应也不放行请求直接卡死超时后前端才报错。4.3 统一返回格式和跨域预检的细节网关层返回401时很多团队的内部协议返回体结构是统一的比如code、message、data三段式。我这里简化成了最普通的JSON结构实际项目中你可以抽一个公共的Result对象但要注意网关层尽量不要依赖下游服务里的统一响应类不然网关和微服务之间的jar包耦合会越来越重。我见过团队把用户服务的Result类直接引入网关后来重构时两边互相拖累很不舒服。另一个值得注意的点是跨域预检。前端项目在调用接口时浏览器会先发送一个OPTIONS请求探路。如果网关的全局过滤器没有放行OPTIONS这个预检请求就会被登录校验拦截返回401甚至403浏览器就会报“CORS policy”错误而且这个报错往往不会提示你具体原因。上面代码里我也专门加了对OPTIONS的放行判断配合application.yml里的globalcors配置才能正常工作。这里有个容易翻车的地方如果你们的前端网关前面还有一层Nginx做域名转发那么跨域配置的allowedOriginPatterns不能写死前端域名最好用*配合allowCredentials: true的模式同时确认Nginx也把Origin和Authorization这两个头正确传下来了。我在排障时多次发现网关配置没问题问题出在Nginx把headers过滤掉了。4.4 把用户信息透传给下游服务网关校验通过后下游服务如何拿到当前用户信息答案就是请求头。我习惯统一约定X-User-Id、X-Username这两个内部请求头。下游服务用一个拦截器从请求头里读取放到ThreadLocal里业务代码直接用。下游服务的用户上下文读取代码可以这样写Component public class UserContextInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String userId request.getHeader(X-User-Id); String username request.getHeader(X-Username); if (StringUtils.hasText(userId)) { UserContext.set(userId, username); } return true; } Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }这里的ThreadLocal存储一定要注意清理。app服务都是线程池复用线程的如果不在afterCompletion里调用clear()高并发下会出现用户A请求里带着用户B的信息这个bug非常隐蔽而且线上偶发排查起来极其痛苦。还有一个透传的坑存在于微服务间的Feign调用。服务A接收网关的请求头后如果服务A再通过Feign调用服务BFeign默认不会把网关传过来的X-User-Id头继续传递给服务B。这时候需要在Feign的RequestInterceptor里手动透传Configuration public class FeignHeaderConfig { Bean public RequestInterceptor requestInterceptor() { return template - { ServletRequestAttributes attrs (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attrs ! null) { HttpServletRequest request attrs.getRequest(); String userId request.getHeader(X-User-Id); if (StringUtils.hasText(userId)) { template.header(X-User-Id, userId); } } }; } }有些团队会选择用Seata或者Sleuth这类组件来传递链路上下文但对于简单的登录用户信息手动透传请求头是成本最低、最可控的方案。安全方面我额外建议在网关层对这三个内部请求头做一步操作清掉客户端传进来的同名头。因为外部请求完全可以伪造X-User-Id如果不先移除再自己添加下游服务可能被恶意伪造用户身份。上面代码里用request.mutate()重新添加header之前最好先调用headers.remove(X-User-Id)。5. 常见问题排查与踩坑实录5.1 高频报错速查表把网关相关的问题整理成一张速查表方便你后续排查时直接对照现象可能原因解决方案启动报“Spring MVC found on classpath, which is incompatible with Spring Cloud Gateway”网关引入了spring-boot-starter-web排除web依赖保留WebFlux路由转发404没配StripPrefix路径前缀没去掉在路由filters里加StripPrefix1前端报CORS跨域但网关配置看着没问题OPTIONS预检被全局过滤器拦截了在全局过滤器最前面放行OPTIONS请求卡死不返回Redis取不到值时没走switchIfEmpty给Mono加switchIfEmpty兜底解析JWT报SecretKeyTooLong密钥长度不足32字节换成长密钥用Base64或128位随机数下游服务获取不到用户信息请求头被网关丢弃或下游没加拦截器网关mutate后记得重新build exchange下游加解析逻辑服务间Feign调用丢失登录态Feign默认不透传请求头配置RequestInterceptor透传Filter代码改了不生效没加Component注解交给Spring容器管理Gateway才能扫描到5.2 踩坑实录过滤器里操作了阻塞API导致性能骤降有一次压测网关登录校验场景发现TPS始终上不去排查后发现我在GlobalFilter里用了RestTemplate调了一个用户服务接口。在WebFlux的Netty线程模型里RestTemplate是阻塞式的任何一个阻塞操作都会把事件循环线程卡住。并发一高请求挤压整个网关就像堵死了一样。解决办法是把阻塞调用改成响应式调用或者把这类校验信息提前同步到Redis里用ReactiveRedisTemplate去读。这也是我前面一直强调要用reactive版Redis客户端的原因。如果你在网关过滤器里看到try/catch包着一个同步RPC调用几乎可以断定这地方迟早出性能事故。5.3 网关层过滤器设计的一些实用心得过滤器职责划分上我强烈建议保持单一。不要写一个超级过滤器又做登录校验又做参数校验又做加解密最后几百行代码堆在一个类里。每加一个独立逻辑就新建一个GlobalFilter用Order控制先后顺序。这样排错时只要看Order就知道哪个先执行日志也好定位。白名单维护要放在配置中心不要把路径写死在代码里。我一开始把白名单定义成代码常量结果每次新增放行接口都要发版一次网关。后来移到Nacos配置里再配合RefreshScope动态刷新效率高了一截。注意发布变更后要确认配置中心真的刷新成功不要盲目信赖自动刷新机制。服务间传递用户信息的请求头强烈建议用常量类统一管理然后Gateway服务和各微服务引入同一个“内部API常量模块”免得每个服务里各写一份字符串一个字母拼错就查半天。如果我每次都要凭记忆手写X-User-Id迟早有一天会写错。生产环境一定要给网关加访问日志记录请求路径、响应状态码、耗时、调用方IP。出问题的时候这些数据能帮你快速定位是网关拦截了还是路由转发失败。我用的是在过滤器里记录开始时间在chain.filter(exchange).then()的回调里计算耗时的写法对性能影响极小。6. 最后的最后网关登录校验的延伸思路网关登录校验这套方案做顺手之后你会发现很多横切逻辑都可以往网关挪。比如接口级权限校验、限流熔断、黑白名单IP、操作审计日志、接口幂等控制这些都是我后续在网关上一层层加出来的能力。每次加新能力前我都会先问自己这个逻辑是所有服务都需要的吗如果是放到网关才是正确位置如果只有某个服务需要那就用局部过滤器不要污染全局链路。按我个人经验微服务拆分初期最值得做的三件事就是统一日志规范、统一网关认证、统一配置中心。其中网关登录校验收益最快做成之后整个团队的接口安全基线立刻上了一个台阶后面新增十个服务都不用再担心认证问题。你在自己项目里动手时先从最简单的GlobalFilterJWT开始把链路跑通后再慢慢叠加Redis校验、白名单刷新、请求头透传这些增强功能千万不要一上来就追求大而全否则会把自己绕晕。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑