共计 3294 个字符,预计需要花费 9 分钟才能阅读完成。
痛点分析
在传统的微服务架构中,Session 认证机制逐渐暴露出诸多问题。这些问题在系统规模扩大后尤为明显,主要体现在以下几个方面:

-
内存消耗问题 :每个用户的 Session 信息都需要保存在服务端内存中,当用户量达到百万级别时,对内存的消耗非常可观。我曾经在一个项目中遇到过,仅 Session 存储就占用了近 30% 的服务器内存资源。
-
跨服务认证复杂 :当请求需要经过多个微服务时,每个服务都需要验证 Session 的有效性。这要么需要共享 Session 存储,增加了系统复杂度;要么需要重复验证,降低了系统性能。
-
横向扩展困难 :当需要增加服务实例时,Session 的同步和共享成为瓶颈。虽然可以使用 Redis 等分布式缓存来解决,但这又引入了新的依赖和潜在的单点故障。
方案对比
Token 认证方案主要有 JWT 和 Opaque Token 两种,它们各有优缺点:
JWT(JSON Web Token)
- 优点 :
- 自包含:所有必要信息都在 Token 中,减少服务端查询
- 无状态:服务端不需要存储会话信息
-
跨语言:基于标准,各语言都有成熟实现
-
缺点 :
- Token 无法主动失效
- Payload 内容可能被解码(虽然不能篡改)
- 体积较大
Opaque Token
- 优点 :
- 体积小
- 服务端完全控制
-
可以立即失效
-
缺点 :
- 每次验证都需要服务端查询
- 增加了认证服务的压力
选型决策树 :
1. 是否需要立即失效能力?是→Opaque;否→下一步
2. Token 中是否需要携带业务信息?是→JWT;否→Opaque
3. 对网络传输效率敏感?是→Opaque;否→JWT
核心实现
JWT 生成示例
public class JwtGenerator {
// 生成 RSA 密钥对
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();}
// 生成 JWT Token
public String generateToken(User user, KeyPair keyPair) {Instant now = Instant.now();
return Jwts.builder()
.setSubject(user.getId())
.claim("roles", user.getRoles())
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(1, ChronoUnit.HOURS)))
.signWith(keyPair.getPrivate(), SignatureAlgorithm.RS256)
.compact();}
}
双 Token 刷新机制
public class TokenService {
// 生成访问 Token 和刷新 Token
public TokenPair generateTokenPair(User user) {String accessToken = generateToken(user, 1, TimeUnit.HOURS);
String refreshToken = generateToken(user, 7, TimeUnit.DAYS);
return new TokenPair(accessToken, refreshToken);
}
// 使用刷新 Token 获取新访问 Token
public String refreshAccessToken(String refreshToken) {if (!validateToken(refreshToken)) {throw new InvalidTokenException("Invalid refresh token");
}
Claims claims = parseToken(refreshToken);
return generateToken(claims.getSubject(), 1, TimeUnit.HOURS);
}
}
性能优化
公钥本地缓存
每次验证 JWT 时,最耗时的操作是获取公钥。可以将公钥缓存在本地:
public class JwtValidator {private final Map<String, PublicKey> publicKeyCache = new ConcurrentHashMap<>();
public boolean validateToken(String token, String keyId) {
PublicKey publicKey = publicKeyCache.computeIfAbsent(keyId,
k -> fetchPublicKeyFromAuthServer(keyId));
// 使用公钥验证 Token...
}
}
Redis 黑名单
对于需要立即失效的 Token,可以使用 Redis 存储黑名单:
public class TokenBlacklist {
private final RedisTemplate<String, String> redisTemplate;
public void addToBlacklist(String token, long expireInSeconds) {redisTemplate.opsForValue().set("blacklist:"+token, "1", expireInSeconds, TimeUnit.SECONDS);
}
public boolean isBlacklisted(String token) {return redisTemplate.hasKey("blacklist:"+token);
}
}
安全防护
防范重放攻击
在 Token 中加入时间戳和随机数:
public String generateToken(User user) {return Jwts.builder()
.setSubject(user.getId())
.claim("nonce", UUID.randomUUID().toString()) // 随机数
.claim("timestamp", System.currentTimeMillis()) // 时间戳
// 其他 claims...
.signWith(privateKey)
.compact();}
敏感操作二次认证
对于敏感操作(如修改密码、支付等),强制要求二次认证:
@PostMapping("/sensitive-operation")
public ResponseEntity<?> sensitiveOperation(@RequestHeader("Authorization") String token,
@RequestBody SensitiveRequest request) {
// 验证主 Token
Claims claims = jwtValidator.validateToken(token);
// 检查二次认证 Token
if (!request.hasValidSecondFactorToken()) {return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorResponse("Second factor authentication required"));
}
// 执行业务逻辑...
}
避坑指南
- 避免存储敏感数据 :Token 可能被解码,不要在 Payload 中存储密码、密钥等敏感信息
- 控制 Payload 大小 :HTTP Header 通常有大小限制(如 8KB),过大的 Token 可能导致问题
- 合理设置有效期 :访问 Token 建议 1 - 2 小时,刷新 Token 建议 7 天
- 使用 HTTPS:防止 Token 在传输过程中被截获
开放问题
Token 的有效期设置是个需要权衡的问题:
– 过短:用户体验差,需要频繁重新登录
– 过长:安全风险增加
如何平衡安全性和用户体验?欢迎在评论区分享你的经验和见解。
