Spring Cloud Gateway + OAuth2 + JWT实现微服务统一认证授权实战

发布时间:2026/9/20 14:24:30
Spring Cloud Gateway + OAuth2 + JWT实现微服务统一认证授权实战 简介基于Spring Cloud Gateway、OAuth2.0与JWT构建的微服务认证授权示例工程面向中高级Java后端与微服务开发者聚焦网关统一鉴权、令牌签发校验、多服务安全接入等常见痛点。工程需整合Nacos注册中心运行前应修改Redis连接与JDBC数据库配置。压缩包共270个文件、约340KB其中204个XML用于Spring配置与MyBatis映射56个Java类构成认证授权与业务核心另有YML环境配置、SQL初始化脚本及PNG效果截图目录划分明确便于按模块查阅。目前已有7161人学习下载。源码完整覆盖网关全局过滤器、授权服务器配置、认证服务、登录控制器、自定义用户信息转换器及Redis缓存工具等关键实现读者可深入理解OAuth2.0授权码/密码模式在微服务中的落地方式掌握Token生成、刷新、校验与网关路由鉴权的完整链路并能借助SQL脚本快速搭建可运行环境是学习Spring Cloud安全体系的优质参考。 Spring Cloud Gateway配合OAuth2.0和JWT实现微服务统一认证授权这套组合在近几年的Java后端项目里出现频率确实非常高。不管是面试被问“微服务接口怎么做权限控制”还是接手一个单体系统准备拆微服务最后大多数团队都会落到这个方案上尤其像若依微服务版这类开源脚手架核心链路基本也是这一套。项目本身要解决的就一件事服务拆开之后登录状态不共享了怎么在网关统一做认证和授权同时让每个下游微服务都拿到可信的用户身份。OAuth2.0负责发令牌JWT作为令牌载体Spring Cloud Gateway做统一校验入口三者一配合就形成了一套无状态的通行证系统。文章会把整套方案的原理、选型理由、可落地代码和踩过的坑都整理出来。适合正在做微服务改造的Java开发也适合准备Spring Cloud面试、想系统理解网关认证链路的读者。1. 整体设计思路拆解认证授权为什么要放在网关层1.1 微服务拆分后认证授权到底乱在哪单体时代认证授权很简单Tomcat里Session一存拦截器一写所有请求都走同一个进程用户登录状态天然共享。但微服务一拆问题立刻冒出来第一Session不共享。服务A和服务B各部署一套用户在A登录了请求到B还是未认证状态除非引入Spring Session做Session集中存储否则每个服务都要维护一份会话数据。哪怕做了集中存储SessionId通过Cookie传递而微服务之间走的是内部调用不一定经过浏览器这套在服务间就玩不转。第二每个服务重复实现认证逻辑。业务服务拆出去之后如果每个服务都写一遍登录校验、权限判断、用户信息获取代码重复不说认证规则一旦升级所有服务都得跟着改发布节奏和风险都成问题。第三权限数据分散。用户名、角色、菜单权限散落在多个服务各自的数据库里想统一管理权限模型非常痛苦。所以微服务架构下认证授权不能由各业务服务自己管必须抽出来变成一个独立的横切关注点。1.2 网关做“统一关卡”的三个核心优势Spring Cloud Gateway作为所有外部请求的流量入口把认证授权放在这一层等于在进城的高速路口设了一个收费站而不是每栋楼下再查一次身份证。第一个优势是请求收敛。不管后端拆成多少个服务外部流量必经网关在这一个节点上完成Token校验合法请求放行非法请求直接拦截下游服务完全不用关心认证细节。第二个优势是用户身份传递标准化。网关校验完Token后可以把用户ID、角色等信息解析出来通过Header统一转发给下游服务。这样业务服务只需要读一个固定的Header就能拿到当前用户身份不需要自己解析JWT耦合度大幅降低。第三个优势是扩展方便。后续想加白名单、限流、灰度发布、接口鉴权在网关过滤器链条上追加就行不用动业务服务。1.3 为什么选OAuth2 JWT而不是Session共享实话实说Session共享方案在早期微服务里也有应用比如Spring Session Redis。这套方案的问题是每次请求都需要查一次Redis高并发下Redis压力很大而且Session是服务端状态服务扩容缩容时要考虑Session迁移和微服务“无状态化”的理念相悖。相比之下OAuth2 JWT的好处是令牌自包含。JWT本身携带用户信息和过期时间网关验签通过后直接就能拿到身份数据完全不依赖会话存储。服务端只要保证签发和验签的密钥安全就能做到大规模水平扩展。从选型逻辑上看OAuth2解决的是“令牌怎么发、怎么换”的协议问题JWT解决的是“令牌长什么样、怎么验证”的格式问题网关解决的是“令牌在哪里验”的入口问题。三者各管一段链路清晰这也是这套方案能在微服务领域成为主流的重要原因。2. 核心概念梳理JWT格式、授权模式与网关过滤链路2.1 JWT的三段式结构别再只会说“一个加密串”很多人提到JWT就说“一个加密的字符串”这在面试里很容易被追问到底层结构。JWT由三部分组成用点号分隔Header存放令牌类型和签名算法常见的就是{alg:HS256,typ:JWT}。注意Header只是Base64编码不是加密任何人都能解码看到内容。Payload存放用户信息和声明比如sub主题、exp过期时间、iat签发时间也可以自定义字段如userId、roles。这部分同样是Base64编码所以不要把密码等敏感信息放进JWT。Signature由Header中声明的算法对前两段拼接后的字符串做签名生成。签名的密钥只有服务端知道因此任何对Payload的篡改都会导致验签失败。理解这个结构后再看网上说的“jwt在线解析”本质就是把Header和Payload做Base64解码几秒钟就能看到Token里的内容。这也提醒我们JWT防的是“篡改”而不是“偷看”敏感数据一定要避免入Token。2.2 OAuth2授权模式怎么选授权码模式与客户端模式最常遇到OAuth2.0定义了四种授权模式实际做微服务网关认证时最少要分清两种授权码模式适用于有前端页面的Web应用比如用户通过浏览器登录跳转到认证服务认证成功后授权码换Token。这是目前Spring Authorization Server里最主流的模式适合真实用户参与登录的场景。客户端模式适用于服务与服务之间的调用没有用户参与直接客户端凭证换Token适合内部的系统间接口调用。另外两种是简化模式和密码模式。密码模式在OAuth2官方文档里其实只建议在高度信任的客户端场景下使用但很多内部管理系统简化开发时仍然采用比如前后端分离项目中前端拿用户名密码换Token。简化模式现在已经不太推荐了因为安全性弱容易在URL中暴露Token。做微服务网关认证的时候需要先想清楚自己属于哪种场景。内部系统、用户登录少密码模式或授权码模式都可以对外开放API用客户端模式或者更严谨地叠一层OIDCOpenID Connect协议。实际上很多系统用的就是Spring Security OAuth2体系热词里提到的cas oauth2 oidc也是围绕着这个生态。2.3 网关过滤器的执行位置与链路顺序Spring Cloud Gateway的认证逻辑通常写在GlobalFilter里。一个请求进来先经过Gateway的HandlerMapping定位到路由然后由过滤器链依次处理。我们自定义的认证过滤器一般是链路上的第一道关卡而且要通过Ordered接口控制优先级数值越小越先执行。典型的执行顺序是认证过滤器先取Token校验合法性再从Token里解析用户信息然后把用户信息写进请求Header继续向下传递最后路由到具体业务服务。这样业务服务和JWT彻底解耦拿到Header里的用户ID就能干活。需要特别强调的是网关里自定义的Header必须由网关统一覆盖不能信任客户端传进来的Header。比如X-User-Id这个Header客户端是可以手工伪造的。网关在转发前要做一次重写用JWT里解析出来的真实身份覆盖同名Header防止越权操作。3. 动手实现认证服务、网关Filter与配置三板斧3.1 工程结构设计与依赖版本选型一个最精简的实现至少需要三个服务认证服务负责用户登录、颁发JWT、刷新Token最简模式下可以不依赖数据库用内存用户演示。网关服务Spring Cloud Gateway工程包含路由转发和全局认证过滤器。资源服务模拟业务服务提供需要认证的接口验证Header携带的用户信息是否生效。依赖版本上Spring Boot 2.7.x Spring Cloud 2021.x是比较稳妥的组合对应的Spring Security和OAuth2相关依赖版本都是经过大量项目验证的。如果要用Spring Boot 3.x Spring Authorization Server需要注意API变化很大比如WebSecurityConfigurerAdapter已经被废弃授权服务器单独成为Spring项目跟Spring Security OAuth2的旧写法完全不一样。初次实现我建议先对照成熟版本跑通链路再考虑升级。核心依赖大致如下网关服务引入spring-cloud-starter-gateway认证服务引入spring-boot-starter-web、spring-boot-starter-security和spring-security-oauth2-jose资源服务引入spring-boot-starter-web和spring-security-oauth2-resource-server。JJWT库推荐用io.jsonwebtoken:jjwt-api配合jjwt-impl和jjwt-jackson。3.2 认证服务登录接口与JWT签发认证服务里最关键的方法就是生成Token。用JJWT实现时先构建Claims放入用户基本信息再设置过期时间最后用密钥签名public String generateToken(UserDetail user) { MapString, Object claims new HashMap(); claims.put(userId, user.getId()); claims.put(roles, user.getRoles()); return Jwts.builder() .setClaims(claims) .setSubject(user.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() expireTime)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact(); }上面代码里有一个值得注意的点密钥不能写成固定字符串写死在代码里必须从配置中心或者环境变量读取。实际项目中有些团队把密钥硬编码在application.yml里这是很危险的习惯一旦代码仓库泄露等于把Token签发权交出去了。过期时间的设计也有讲究。太短会导致用户频繁重新登录太长又增加Token泄露的窗口期。常见做法是AccessToken设30分钟到2小时再配合RefreshToken实现续签。关于续签后面专门讲这里先不展开。3.3 网关过滤器白名单、验签与用户信息传递网关服务的核心是一个全局过滤器代码逻辑可以分成三步Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getURI().getPath(); // 1. 白名单直接放行 if (whiteList.contains(path)) { return chain.filter(exchange); } // 2. 获取并校验Token String token resolveToken(request); if (StringUtils.isBlank(token) || !jwtUtil.validateToken(token)) { return unauthorizedResponse(exchange); } // 3. 解析用户信息覆盖Header后放行 Claims claims jwtUtil.parseToken(token); ServerHttpRequest mutatedRequest request.mutate() .header(X-User-Id, claims.get(userId).toString()) .header(X-User-Roles, claims.get(roles).toString()) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } }白名单是必须的。登录接口本身不能被拦住否则没Token的用户永远无法登录这就死循环了。常见白名单包括/auth/login、/auth/refresh、验证码接口、静态资源等。另外给一个查漏补缺的建议前后端分离项目里一定要把CORS预检请求OPTIONS请求加进白名单或者单独处理。浏览器发送跨域请求前会先发一个OPTIONS预检这个请求通常不带Token如果网关拦掉了前端跨域直接失败。我踩过一次这个坑花了半天排查才发现是预检请求被认证过滤器拒了。3.4 资源服务低成本读取用户信息资源服务不需要再解析JWT直接从Header里取用户信息即可。原因有两点一是网关已经验过签走到下游的请求可信度高二是保持业务服务简单不重复引入JWT解析逻辑。GetMapping(/order/list) public Result list(HttpServletRequest request) { String userId request.getHeader(X-User-Id); // 按userId查询订单列表 }这里有一个安全细节要提醒资源服务如果完全信任网关传递的Header那么网关到资源服务之间的链路必须可控。如果在云上部署服务间调用走的是内网VPC风险相对可控如果中间还有其他网关或者服务变多可以考虑用mTLS或者加一层内部Token来保护链路。4. 避坑实战安全加固、Token续签与常见问题排查4.1 高频问题排查速查表实际开发中用这套方案我整理了几个出现频率最高的问题问题现象排查方向解决方案登录接口一直401白名单没配或路径匹配错误检查白名单路径是否和路由前缀一致前端跨域请求失败OPTIONS预检请求被过滤器拦截白名单放行OPTIONS请求网关转发后业务服务拿不到用户信息Mutation Header的时机不对确认是用mutate后的Request进行后续过滤Token过期时间不生效服务器时钟不一致或秒/毫秒单位写错检查setExpiration入参是否用了毫秒每次重启用户全部要重新登录JWT密钥或盐值每次启动随机生成配置固定密钥或从配置中心读取验签偶尔报错多实例之间密钥不一致确认所有服务实例读取同一份密钥配置其中密钥不一致的问题最隐蔽特别是用Kubernetes部署多实例时如果密钥来自本地环境变量而不是统一配置中心就会出现同一个Token在这台机器验签通过、在另一台机器验签失败的情况。4.2 JWT安全风险算法混淆、密钥泄露与修复建议JWT在安全上最容易踩的坑是算法混淆攻击。伪造一个alg: none的JWT服务端如果直接从Token头部读取算法并信任它就可能跳过验签过程。修复方法很简单服务端验签时不要信任Token里声明的算法而是代码里明确指定允许的算法白名单。另一个高频漏洞是短密钥。HS256算法用的密钥如果太短很容易被暴力破解。要求是密钥长度至少256位32字节并且要用强随机数生成。项目里我看到有的团队用“123456”这种明文当密钥一查一个准。修复建议总结成三条验签前强制指定算法拒绝alg: none密钥长度不低于32字节并用环境变量或配置中心管理严格校验exp过期字段发生过期时间字段缺失或设置为永久的案例。另外JWT是Base64编码的任何人解出来就能看到Payload里的内容不要把手机号、身份证、密码这类敏感数据放进Token。4.3 Token续签与强制登出的落地做法JWT无状态带来一个痛点签发之后没办法主动让它失效。用户修改密码、管理员封禁账号、用户点击退出登录这些场景都需要Token立即失效。普遍的做法是引入RefreshToken机制。AccessToken短时间有效比如30分钟RefreshToken有效期长一些比如7天。AccessToken过期后客户端拿着RefreshToken去认证服务换新的AccessToken。RefreshToken一般存储在Redis里可以在签发时绑定用户刷新时校验。强制登出的做法更直接做一张JWT黑名单登出时把Token的jti或者用户ID写进Redis设置过期时间等于Token剩余有效期。网关校验Token时先查一下黑名单命中就拒绝。这套方案牺牲了一点无状态性但换来了可管控性在真实业务里是值得的。滑动续签也是很多项目在用的思路只要用户活跃就动态刷新过期时间不活跃了Token自然失效避免用户长时间无操作后被强制下线。关键是要设计好“活跃”的判定规则不能每个请求都刷一次Redis否则Redis压力会比较大。4.4 限流与网关扩展热词里提到“eurekagateway的springcloud如何限流”这里顺带聊一下。网关做限流是常规操作Spring Cloud Gateway内置了RequestRateLimiter过滤器配合Redis实现令牌桶限流。限流时要注意按用户维度做隔离不然一个接口被刷把其他人全部拖下水。用前面解析出来的X-User-Id或者IP地址作为限流Key是比较实用的做法。从这里也能看出网关层的价值认证、限流、灰度、日志各种横切需求都汇聚在这一层处理业务服务保持干净架构扩展起来非常顺手。5. 写在最后这套Gateway OAuth2 JWT的方案我前后在好几个项目里落地过踩过的坑比想象中的多。最大的体会有两个一是方案选型要克制满足业务场景即可一个内部管理系统非要用授权码模式加各种安全加固反而增加维护成本二是安全细节不能将就密钥管理、算法白名单、Header覆盖这三件事是底线做不好后面的工作全是白搭。如果你正准备在项目里落地这套架构建议先用最简的三服务模型把链路跑通再逐步叠加白名单、限流、续签、权限模型这些功能。链路通了后面的扩展都很顺链路里任何一个环节有问题调试起来就非常折磨人。最后再分享一个小技巧网关层做认证时把Token解析和用户信息传递封装成独立组件通过依赖引入网关工程多套环境复用起来特别方便也不容易在复制代码时改漏逻辑。本文还有配套的精品资源点击获取