微服务统一认证:JWT+网关全局鉴权体系
每个微服务都自己搞一套登录校验——这跟每个房间都装一把不同的锁有什么区别?用户得带一大串钥匙。今天用 JWT + 网关搞一套全局鉴权,一锁通所有。
一、微服务认证方案的演进
单体应用时代,用户登录后服务器在 Session 里存状态,客户端带个 Cookie 就行。微服务化后这个模式碰到了问题:
Session + Cookie 的困境: ┌─ order-service ─┐ 用户 ←→ 浏览器 ←→ 网关 ├─ product-service├ 谁都不认识谁的 Session! └─ user-service ──┘ 问题1:Session 存在单机内存,其他服务访问不到 问题2:即使共享 Redis Session,跨域 Cookie 也是个坑 问题3:服务间调用时,下游不知道上游的用户身份1.1 三种方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| Session + Cookie | 服务端存状态,Cookie 传 sessionId | 可控性强、可主动踢人 | 不适合分布式、跨域困难 |
| JWT(推荐) | 客户端存 Token,自包含用户信息 | 无状态、跨域友好、适合微服务 | 无法主动失效、Token 体积大 |
| OAuth2 | 第三方授权+资源服务器分离 | 适合开放平台 | 复杂度高、学习曲线陡 |
JWT 在微服务场景的优势:无状态,不需要集中式 Session 存储,Token 本身就携带用户信息,任何服务拿到 Token 都能独立校验。
二、JWT 原理拆解
JWT(JSON Web Token)长这样:
eyJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOjEsInJvbGUiOiJhZG1pbiJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 拆开来看,就是三段 Base64 编码用点号连接:┌──────────────────────┬──────────────────────────┬─────────────────────────┐ │ Header │ Payload │ Signature │ ├──────────────────────┼──────────────────────────┼─────────────────────────┤ │ { │ { │ HMACSHA256( │ │ "alg": "HS256", │ "userId": 1, │ base64(header) + │ │ "typ": "JWT" │ "role": "admin", │ "." + │ │ } │ "exp": 1699999999 │ base64(payload), │ │ │ } │ secret │ │ │ │ ) │ └──────────────────────┴──────────────────────────┴─────────────────────────┘- Header:声明算法和类型(HS256 = HMAC-SHA256)
- Payload:存放声明(claims),比如用户 ID、角色、过期时间。Payload 只是 Base64 编码,不是加密!千万别放密码进去。
- Signature:用 Header + Payload + 密钥算出签名,防篡改。任何人改了 Payload,签名就对不上了。
校验流程:服务器收到 JWT → 用同样的算法对 Header+Payload 算签名 → 对比 Signature → 一致则信任 Payload。
三、JWT 生成与解析(jjwt)
引入 jjwt 库:
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.12.3</version></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.12.3</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>0.12.3</version><scope>runtime</scope></dependency>JWT 工具类:
@ComponentpublicclassJwtUtil{@Value("${jwt.secret:MySecretKeyForJWT@2024}")privateStringsecret;@Value("${jwt.access-token-expiration:1800}")// 30 分钟privatelongaccessTokenExpiration;@Value("${jwt.refresh-token-expiration:604800}")// 7 天privatelongrefreshTokenExpiration;privateSecretKeygetSignKey(){returnKeys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));}// 生成 Access TokenpublicStringgenerateAccessToken(LonguserId,Stringrole){returnJwts.builder().subject(String.valueOf(userId)).claim("role",role).claim("type","access").issuedAt(newDate()).expiration(newDate(System.currentTimeMillis()+accessTokenExpiration*1000)).signWith(getSignKey()).compact();}// 生成 Refresh TokenpublicStringgenerateRefreshToken(LonguserId){returnJwts.builder().subject(String.valueOf(userId)).claim("type","refresh").issuedAt(newDate()).expiration(newDate(System.currentTimeMillis()+refreshTokenExpiration*1000)).signWith(getSignKey()).compact();}// 解析 TokenpublicClaimsparseToken(Stringtoken){returnJwts.parser().verifyWith(getSignKey()).build().parseSignedClaims(token).getPayload();}// 校验 Token 是否有效publicbooleanvalidateToken(Stringtoken){try{parseToken(token);returntrue;}catch(JwtException|IllegalArgumentExceptione){returnfalse;}}// 从 Token 中获取用户 IDpublicLonggetUserId(Stringtoken){returnLong.parseLong(parseToken(token).getSubject());}}四、统一鉴权架构
用户 网关 微服务 ┌────────┐ ┌─────────────────────────┐ ┌──────────┐ │ 登录请求 │───→│ ① 校验用户名密码 │ │ │ │ │ │ ② 签发 JWT │ │ user- │ │ │←───│ ③ 返回 Token │ │ service │ └────────┘ └─────────────────────────┘ └──────────┘ │ ┌────────┐ ┌─────▼───────────────────┐ ┌──────────┐ │ 业务请求 │───→│ ④ 全局过滤器校验 JWT │ │ order- │ │(带Token)│ │ ⑤ 解析用户信息 │──→│ service │ │ │ │ ⑥ 透传用户信息到 Header │ │ │ └────────┘ └─────────────────────────┘ └──────────┘4.1 网关全局过滤器
Spring Cloud Gateway 中实现全局 JWT 校验:
@Component@Slf4jpublicclassJwtAuthFilterimplementsGlobalFilter,Ordered{@AutowiredprivateJwtUtiljwtUtil;// 不需要校验的路径(白名单)privatestaticfinalList<String>WHITE_LIST=Arrays.asList("/api/user/login","/api/user/register","/api/public/");@OverridepublicMono<Void>filter(ServerWebExchangeexchange,GatewayFilterChainchain){Stringpath=exchange.getRequest().getURI().getPath();// 白名单直接放行if(isWhitePath(path)){returnchain.filter(exchange);}// 从请求头获取 TokenStringtoken=exchange.getRequest().getHeaders().getFirst("Authorization");if(token==null||!token.startsWith("Bearer ")){returnunauthorized(exchange,"未携带Token");}token=token.substring(7);// 去掉 "Bearer " 前缀// 校验 Tokenif(!jwtUtil.validateToken(token)){returnunauthorized(exchange,"Token无效或已过期");}// 解析用户信息LonguserId=jwtUtil.getUserId(token);Claimsclaims=jwtUtil.parseToken(token);Stringrole=claims.get("role",String.class);// 将用户信息写入请求头,透传给下游微服务ServerHttpRequestrequest=exchange.getRequest().mutate().header("X-User-Id",userId.toString()).header("X-User-Role",role).build();log.debug("用户 {} 请求 {}",userId,path);returnchain.filter(exchange.mutate().request(request).build());}privatebooleanisWhitePath(Stringpath){returnWHITE_LIST.stream().anyMatch(path::startsWith);}privateMono<Void>unauthorized(ServerWebExchangeexchange,Stringmessage){ServerHttpResponseresponse=exchange.getResponse();response.setStatusCode(HttpStatus.UNAUTHORIZED);response.getHeaders().setContentType(MediaType.APPLICATION_JSON);Stringbody="{\"code\":401,\"message\":\""+message+"\"}";DataBufferbuffer=response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));returnresponse.writeWith(Mono.just(buffer));}@OverridepublicintgetOrder(){return-100;// 高优先级,在其他过滤器之前执行}}关键点:用户信息通过自定义请求头透传。网关校验通过后,把 userId 和 role 塞到X-User-Id和X-User-Role头里,下游服务直接从请求头取就行,不需要再解析 JWT。
4.2 微服务端获取用户信息
既然网关已经帮你解析好了,下游服务直接取:
@RestController@RequestMapping("/api/order")publicclassOrderController{@GetMapping("/my-orders")publicList<Order>getMyOrders(@RequestHeader("X-User-Id")LonguserId){returnorderService.listByUserId(userId);}}更优雅的做法——自定义注解 + 参数解析器:
// 自定义注解@Target(ElementType.PARAMETER)@Retention(RetentionPolicy.RUNTIME)public@interfaceCurrentUser{}// 参数解析器:自动从请求头注入用户信息@ComponentpublicclassCurrentUserResolverimplementsHandlerMethodArgumentResolver{@OverridepublicbooleansupportsParameter(MethodParameterparameter){returnparameter.hasParameterAnnotation(CurrentUser.class);}@OverridepublicObjectresolveArgument(MethodParameterparameter,ModelAndViewContainermavContainer,NativeWebRequestwebRequest,WebDataBinderFactorybinderFactory){HttpServletRequestrequest=webRequest.getNativeRequest(HttpServletRequest.class);LonguserId=Long.parseLong(request.getHeader("X-User-Id"));Stringrole=request.getHeader("X-User-Role");UserContextcontext=newUserContext();context.setUserId(userId);context.setRole(role);returncontext;}}// 使用@GetMapping("/my-orders")publicList<Order>getMyOrders(@CurrentUserUserContextuser){returnorderService.listByUserId(user.getUserId());}五、Token 刷新机制
Access Token 设太长不安全(泄露后风险期长),太短用户体验差(频繁登录)。业界标准做法:双 Token 机制。
Access Token 过期 Refresh Token 过期 (30分钟) (7天) │ │ ────▼──────────────▶ 时间线 ──────────────────▼──── │ │ │ 正常使用 用 Refresh Token 需要重新登录 换取新 Access Token刷新接口实现:
@PostMapping("/api/user/refresh")publicResult<Map<String,String>>refresh(@RequestParamStringrefreshToken){// 校验 Refresh Tokenif(!jwtUtil.validateToken(refreshToken)){thrownewBizException("Refresh Token 无效或已过期,请重新登录");}Claimsclaims=jwtUtil.parseToken(refreshToken);if(!"refresh".equals(claims.get("type",String.class))){thrownewBizException("Token 类型错误");}// 检查 Refresh Token 是否在黑名单中(已注销)if(tokenBlacklistService.isBlacklisted(refreshToken)){thrownewBizException("Token 已注销,请重新登录");}LonguserId=Long.parseLong(claims.getSubject());// 签发新的一对 TokenStringnewAccessToken=jwtUtil.generateAccessToken(userId,claims.get("role",String.class));StringnewRefreshToken=jwtUtil.generateRefreshToken(userId);// 旧的 Refresh Token 加入黑名单(防止被恶意重复使用)tokenBlacklistService.addToBlacklist(refreshToken,jwtUtil.getExpiration(refreshToken));Map<String,String>tokens=newHashMap<>();tokens.put("accessToken",newAccessToken);tokens.put("refreshToken",newRefreshToken);returnResult.success(tokens);}六、Token 黑名单:处理注销
JWT 的最大软肋——无法主动失效。Token 发出去后,在有效期内就是一张有效的通行证。
解决方案:用 Redis 维护一个黑名单,登录时存进去,网关校验时检查:
@ServicepublicclassTokenBlacklistService{@AutowiredprivateStringRedisTemplateredisTemplate;privatestaticfinalStringBLACKLIST_PREFIX="jwt:blacklist:";// 加入黑名单,TTL 设为 Token 剩余有效期publicvoidaddToBlacklist(Stringtoken,Dateexpiration){longttl=expiration.getTime()-System.currentTimeMillis();if(ttl>0){redisTemplate.opsForValue().set(BLACKLIST_PREFIX+token,"1",ttl,TimeUnit.MILLISECONDS);}}publicbooleanisBlacklisted(Stringtoken){returnBoolean.TRUE.equals(redisTemplate.hasKey(BLACKLIST_PREFIX+token));}}这样,用户注销时把当前 Token 扔进黑名单,网关校验时发现黑名单命中就直接拒绝。
七、安全最佳实践
- Payload 不存敏感信息:Payload 只是 Base64 编码,不是加密。密码、手机号、身份证一律不往 Token 里塞
- 密钥强弱决定安全高低:HS256 场景下,密钥至少 32 位随机字符。生产环境用 RS256(非对称加密)更安全
- 全链路 HTTPS:Token 在 HTTP 头里明码传输,不用 HTTPS 等于裸奔
- Access Token 短时效:15~30 分钟为宜。配合 Refresh Token 保证用户体验
- 前端存储:Web 端放 HttpOnly Cookie(防 XSS),移动端放 Keychain/Keystore。不要放 localStorage
总结
JWT + 网关全局鉴权是微服务认证的黄金搭档——网关作为统一入口校验 Token、解析用户身份、透传给下游,每个微服务无需重复写鉴权逻辑。配合双 Token 刷新和 Redis 黑名单,安全性和用户体验兼得。