ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

微服务网关统一认证实战:SpringCloud Gateway + OAuth2.0 + JWT 全流程解析

微服务网关统一认证实战:SpringCloud Gateway + OAuth2.0 + JWT 全流程解析 简介面向微服务安全场景这套源码项目以 Spring Cloud Gateway 为统一入口结合 OAuth2.0 授权协议与 JWT 无状态令牌实现了登录认证、令牌签发、网关过滤和资源服务鉴权的完整链路。适合拥有 Spring Boot/Cloud 基础、正在搭建统一认证中台的开发者学习参考压缩包共 270 个文件、大小仅 340KB其中以 204 个 XML 配置文件与 56 个 Java 源码文件为主另有少量 YAML 配置、SQL 脚本、PNG 结构图和 Markdown 说明整体轻量且目录层次清晰。已有 7161 人学习下载通过研读网关全局过滤器、Token 服务、授权服务器配置、用户信息转换器等核心代码可以理解认证中心与业务服务如何协作按照说明修改 Redis、JDBC 连接并部署 Nacos 后即可运行认证服务和资源服务完成实际操作。从网关拦截到令牌校验再到用户信息转换核心链路均有对应源码对照便于在毕业设计、项目初始化或微服务权限模块中快速上手。 干微服务最尴尬的阶段不是拆分的时候而是拆完之后联调的那两周。服务间接口还没理清楚认证授权先堵在路上了——每个接口都得知道你是谁、你够不够格调我。我接手过一个拆了十几个服务的项目认证逻辑散得到处都是A服务自己写了一套登录B服务用SessionC服务直接明文传用户ID。后来我把这套逻辑统一收敛到网关层用 SpringCloud Gateway OAuth2.0 JWT 把认证授权彻底抽出来业务服务从此不再关心你是谁只关心你要干什么。这篇文章就把这套方案的完整落地过程写清楚为什么必须放在网关、认证链路怎么转、网关过滤器每步在验什么、上生产前要处理哪些坑。适合正在搭微服务基础架构、或者准备重构现有认证逻辑的团队参考内容偏实操能直接照着改。1. 为什么把认证放在网关层而不是每个服务各做一套1.1 分散认证带来的连锁问题很多人一开始觉得每个服务自己做认证更灵活但服务一旦多起来分散认证的代价是成倍增长的。首先是重复代码每个服务都要写一遍Token解析、校验、异常处理同一个Bug要在十几个服务里各修一次。其次是密钥和会话状态难统一有的服务用对称密钥有的用非对称你根本不知道哪个服务的Token泄露了该吊销哪个。最要命的是权限策略不一致同一个接口在一个服务里要求ADMIN角色在另一个服务里居然只校验登录态。网关层做统一认证的核心逻辑很简单所有外部请求必须经过网关那在网关这一层把你是谁的问题解决掉下游服务拿到的就已经是可信身份了。这就像公司前台所有人进门先登记登记完发个工牌各个办公室看到工牌就知道这人能进不能进不需要每个办公室自己再搞一套登记系统。1.2 网关认证的两条技术路线在Spring Cloud技术栈里网关层做认证主流有两条路。一条是引入 Spring Security 的 Resource Server 体系配合 OAuth2.0 的资源服务器模式网关通过 JwtDecoder 自动校验Token。这条路的好处是规范、严谨Security内置了一大堆过滤器但坏处也明显配置复杂、报错信息晦涩新手经常被过滤链的加载顺序绕晕出了问题很难定位是哪个Filter拦截的。另一条是自己在网关写一个 GlobalFilter用 JJWT 或 hutool 之类的工具库解析Token校验签名和过期时间。这条路代码量不大逻辑完全可控出了问题能直接看代码也方便根据业务定制白名单和错误响应。缺点是安全细节要自己注意比如算法混淆攻击、Token注入这些坑都得自己兜住。我最终选的是第二条路。原因很实际我们团队的微服务大部分是内部系统不需要对接第三方开放平台完整的OAuth2.0授权码流程用不上。真正用到的只是签Token、验Token、传身份这三件事自己写一个过滤器更轻、更好维护。如果你要对接微信、GitHub这类第三方登录那就得规规矩矩走OAuth2.0的授权码模式网关这边用Resource Server会更稳。1.3 我的最终技术选型网关SpringCloud Gateway写全局过滤器做统一认证令牌JWTRS256非对称签名私钥只放在认证服务网关只持有公钥认证服务独立的 auth-service负责登录、发Token、刷新Token、注销存储Redis 放黑名单和刷新Token用户基础信息放数据库下游服务通过网关透传的请求头拿到用户ID和角色信息这套组合的核心理念是认证服务只认密码网关只认签名业务服务只认请求头。每一层只干一件事出了问题看日志就能知道卡在哪一环。2. 从登录到放行认证链路是怎么转起来的2.1 登录服务只负责发Token整个链路的第一步发生在 auth-service。用户带着用户名密码请求登录接口认证服务校验通过后生成JWT返回给前端。这个服务的职责就到此为止——它不关心用户之后调了什么接口也不维护任何会话状态。很多人在这一步容易把OAuth2.0想复杂其实在我们这种内部系统场景里它本质上就是用密码换Token。签发Token的代码大致长这样String token Jwts.builder() .setSubject(userId) .claim(username, user.getUsername()) .claim(roles, roleList) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7200_000)) .signWith(privateKey, SignatureAlgorithm.RS256) .compact();这里有几个设计细节值得注意。Subject字段我放的是用户ID不是用户名因为用户名可能变更而ID是永久的。roles放在claim里这样网关校验完可以直接把角色透传给下游下游做细粒度权限判断时不用再查一次用户表。过期时间设的是两小时这个值要根据业务场景调太短用户频繁重新登录太长Token泄露的风险窗口就大。2.2 Token里到底该放什么、不该放什么JWT的Payload是明文Base64编码的任何拿到Token的人都能解出来看所以敏感信息一律不能放。我见过有人把手机号、身份证号甚至密码摘要塞进claim这等于把用户隐私打印在工牌上到处发。Claim是否建议放原因用户ID (sub)建议身份唯一标识下游需要用户名建议展示和日志用变更不频繁角色列表建议网关统一从Token取角色做粗粒度授权权限标识视情况权限变化频繁的慎放Token内权限是快照手机号/邮箱不建议明文可读泄露面大密码/密钥绝对禁止一旦Token泄露等于账号泄露放角色的逻辑要提前想清楚Token里的角色是签发时刻的快照如果用户在下线期间被改了角色旧Token在过期前依然带着老角色。接受这个现实然后通过设置合理的过期时间来控制风险窗口而不是试图让JWT变得可以动态撤销——那是后面要讲的另一个话题。2.3 请求在网关的完整流转Token签发之后前端每次请求都会在Header里带上Authorization: Bearer token。请求到达网关时全局过滤器先执行校验校验通过后把用户信息写入新的请求头再转发给下游服务。下游服务收到的是已经洗白过的请求带着X-User-Id、X-Username、X-User-Roles这三个头。这里要特别注意下游服务绝对不能直接信任前端传的任何自定义 Header因为请求在到达网关之前可能被伪造。网关在转发前应该把外部传入的这几个Header清掉再写入解析出来的可信值。如果你拿到的基础代码没做这一步赶紧补上——这是很常见的越权漏洞来源。3. 网关过滤器里到底验什么3.1 白名单机制要先于校验网关过滤器第一步不是验Token而是判断这个请求需不需要验。登录接口、注册接口、验证码接口、静态资源、网关自身的健康检查端点这些都得放进白名单。白名单的匹配逻辑要支持前缀匹配和精确匹配我用的是配置项驱动auth: whitelist: - /auth/login - /auth/refresh - /auth/captcha - /actuator/health - /public/**白名单这步写起来简单但坑不少。最容易犯的错是白名单路径写得太宽比如把/public/**写成了/**等于所有接口都不鉴权了。还有路径大小写问题、上下文路径context-path没算进去导致匹配不上。我建议白名单匹配用 AntPathMatcher并且启动时打印一份实际生效的白名单列表方便核对。3.2 JWT校验的四步走白名单过了之后过滤器开始验Token核心逻辑是四步第一步从 Authorization Header 里取出Token。注意Bearer前缀要正确解析大小写要容错取不到Header或格式不对直接返回401。第二步验签名。用网关持有的RSA公钥验证Token签名验证失败直接拒绝。这一步是防伪造的关键如果使用对称密钥HS256公钥和私钥是同一个密钥网关和认证服务都得持有它泄露面就大了。用RS256之后认证服务持私钥签发网关只持公钥验证私钥永远不出认证服务的门。第三步验过期时间。JWT的exp字段是签发时写死的解析时校验当前时间是否在过期时间之前。这里有个细节不同服务的服务器时间可能有偏差校验时我会留一个60秒的时钟偏移余量避免因为毫秒级时间差导致Token提前失效。第四步验签成功后把 claim 里的用户信息提取出来写入转发请求的Header。核心代码不复杂GlobalFilter 的实现大致是Component public class AuthGlobalFilter implements GlobalFilter, Ordered { Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { ServerHttpRequest request exchange.getRequest(); String path request.getURI().getPath(); if (whitelistService.matches(path)) { return chain.filter(exchange); } String token resolveToken(request); if (StringUtils.isBlank(token)) { return unauthorized(exchange, 缺少访问令牌); } try { Claims claims jwtParser.parse(token); if (claims.getExpiration().before(new Date())) { return unauthorized(exchange, 令牌已过期); } ServerHttpRequest mutatedRequest request.mutate() .header(X-User-Id, claims.getSubject()) .header(X-Username, String.valueOf(claims.get(username))) .header(X-User-Roles, String.valueOf(claims.get(roles))) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } catch (JwtException e) { return unauthorized(exchange, 令牌无效); } } }3.3 下游服务怎么消费用户身份网关透传请求头之后下游服务要做的是把这些Header解析成统一的用户上下文对象。我习惯在common模块里提供一个工具类比如UserContext.getUserId()底层从RequestContextHolder取Header。这样业务代码不需要关心Header名也不用每处都写RequestHeader注解。还有一个实战经验如果用的是Feign做服务间调用网关透传的身份信息默认不会跟着Feign请求走。你需要配置一个 Feign 的 RequestInterceptor把当前请求里的用户信息Header复制到Feign的请求模板里否则就会出现网关认了身份服务间调用却丢了身份的诡异问题。4. 真正上生产前必须处理的三个问题4.1 Token续签别让用户两小时掉一次线JWT最大的痛点就是签发之后没法主动让它失效所以续签策略要想清楚。我见过两种主流方案。一种是Refresh Token方案登录时同时发访问Token和刷新Token访问Token两小时过期刷新Token有效期七天存在Redis里。前端在收到401时静默调用刷新接口换新Token。这个方案体验好但要额外处理刷新Token的存储和轮换。另一种是滑动过期只要用户在过期时间之前有操作就重新签发一个更长有效期的Token。实现方式是在认证服务提供一个校验接口网关发现Token剩余时间不足30分钟时调用认证服务换新Token返回给前端。我实际用的是Refresh Token方案。因为滑动过期有个问题——客户端必须把新Token带回来这对前后端分离的项目意味着每次请求都要检查响应头是否有新Token逻辑比较绕。Refresh Token的链路更清晰访问Token过期 → 前端捕获401 → 带Refresh Token调刷新接口 → 拿到新Token重放原请求。写刷新接口时要加一个并发控制防止用户同时开多个Tab导致Refresh Token被并发刷新。我的做法是刷新时校验旧Refresh Token并立即删除然后签发新的Refresh Token这样同一个Refresh Token只能用一次。4.2 注销与黑名单JWT注销是个老大难Token在客户端手里服务端没法直接作废。最简单的办法是前端把Token丢掉但这样服务端仍然会接受这个Token直到过期。对于注销敏感度高的场景比如管理员踢人、改密码后强制下线就得引入黑名单机制。我的做法是在Redis里维护一个黑名单key是Token的 jtiJWT IDvalue是过期时间。用户注销或改密码时把当前Token的jti写进黑名单TTL设置为Token剩余有效期。网关过滤器在校验Token之后、放行之前先查一下这个jti是否在黑名单里在就直接拒绝。Redis黑名单要注意清理策略不能只靠TTL自动过期。高并发下大量注销会堆积很多Key建议定期用Scan批量清理而不是用Keys命令——Keys会阻塞Redis生产环境是事故导火索。4.3 密钥管理从YAML里抠出来签Token的私钥如果写死在配置文件里第一个泄密点就是代码仓库。我见过不止一个项目把私钥直接提交到Git然后整个内网都能解出Token。正确的做法是私钥只放在认证服务的环境变量或专门的密钥管理服务里网关只保留公钥。我们用的是JKSJava KeyStore文件启动时通过spring.config.import从配置中心拉取公钥部分则在网关配置里通过classpath:引用。密钥轮换要考虑新旧公钥同时生效的过渡期最简单的手段是在JWT里带一个kidKey ID字段网关根据kid选择对应的公钥解析这样就能平滑切换密钥。5. 踩坑记录那些网上教程不会告诉你的细节5.1 算法混淆攻击是最容易被忽略的洞网上搜JWT 安全漏洞出现频率最高的就是算法混淆攻击。攻击原理是如果服务端用RS256公钥验证签名但解析库允许攻击者把算法改成HS256而HS256使用同一个密钥既签名又验签那攻击者就可以拿公钥当HMAC密钥来伪造Token。防这个坑有两个层面。第一在解析端明确指定只接受RS256不要信任Token头里的alg字段第二升级到新版解析库JWT库早在2018年就默认禁止了算法降级。我用的是JJWT 0.11.x它会在解析时强制校验alg与预期的一致性。如果你接手的老项目还在用旧版库先干这件事比加什么复杂防护都值钱。5.2 网关过滤器顺序与异常处理SpringCloud Gateway的过滤器有优先级之分GlobalFilter通过Ordered接口控制执行顺序。认证过滤器应该排在比较前面的位置我用的是Ordered.HIGHEST_PRECEDENCE 10。这里有个坑是 CORS 的处理顺序如果CORS过滤器排在认证之后跨域预检请求OPTIONS请求会先撞上认证过滤器而没有自定义Header的OPTIONS请求会被直接打回导致前端控制台报CORS错误。解决方法是把OPTIONS请求也放进白名单或者在认证过滤器里先判断HttpMethod.OPTIONS直接放行。另外认证失败时返回的错误结构要保持统一我封装了一个ResultWrapper固定返回{code: 401, message: ...}这样的JSON方便前端统一处理。5.3 真出问题时怎么排查网关认证出问题最有效的排查方式是把过滤器链上的日志打全。我会在认证过滤器的每个关键节点输出日志请求路径、是否命中白名单、是否取到Token、验签是否通过、放行后透传了哪些Header。开生产环境日志时用debug级别配合logging.level.org.springframework.cloud.gatewayDEBUG能看到完整的路由和过滤器执行链路。有一类问题特别容易让人抓狂同一套代码本地环境好好的测试环境一直401。最后查下来是网关和认证服务的系统时间差了五分钟JWT的exp校验直接失败。给所有服务器统一启用NTP时间同步这是我踩过最不值钱的坑。另外提一句网上的若依微服务版本代码用了类似的自定义网关过滤器思路如果你用它起步记得先检查它有没有做上面的Header清洗和算法锁定这两个点很多人改代码时会弄丢。做微服务认证这件事不是把代码跑通就算完而是要把每一层的信任边界画清楚然后守住它。我在实际项目里把这套方案落地之后最直观的变化是新增业务服务不再需要关心认证逻辑新服务只需要解析请求头里的用户上下文就能干活。后续要扩展的话可以把粗粒度授权的动态规则比如按URL配置角色访问控制从代码里挪到配置中心网关每次校验时拉取规则这样连角色变更都不需要改代码发版了。先把认证这层稳下来后面做细粒度权限也好做多租户也好都只是在这个基座上往上搭。本文还有配套的精品资源点击获取
返回列表