共计 2596 个字符,预计需要花费 7 分钟才能阅读完成。
背景痛点
在微服务架构中,access token 的安全存储和高效访问是一个常见但棘手的问题。随着服务拆分和分布式系统的复杂性增加,传统的单体应用中的 token 管理方式往往无法满足需求。以下是几个典型的痛点:

-
跨服务验证 :微服务架构下,一个请求可能需要经过多个服务,每个服务都需要验证 token 的有效性。如果每次验证都要访问中央存储,会带来显著的性能开销。
-
性能瓶颈 :高并发场景下,token 的存储和读取可能成为系统瓶颈。尤其是在用户量激增时,频繁的 token 验证请求可能导致存储系统不堪重负。
-
安全性要求 :token 一旦泄露,攻击者可能通过重放攻击(replay attack)冒充合法用户。因此,token 的存储方案不仅要高效,还需要具备防篡改和防重放的能力。
方案对比
针对 access token 的存储,常见的方案有三种:内存存储、Redis 集群和数据库。以下是它们的核心指标对比:
| 指标 | 内存存储 | Redis 集群 | 数据库 |
|---|---|---|---|
| QPS | 极高(10 万 +) | 高(5 万 +) | 低(1 万以下) |
| TTL 管理 | 手动管理 | 自动过期 | 手动清理 |
| 集群同步 | 困难 | 支持 | 支持 |
| 安全性 | 低(易丢失) | 中(需配置) | 高(持久化) |
从对比中可以看出,内存存储虽然性能最高,但缺乏持久化和集群同步能力;数据库虽然安全,但性能较差;Redis 集群在性能和安全性之间取得了较好的平衡,但在极端情况下(如 Region 级故障)可能面临挑战。
混合方案实现
为了兼顾性能和安全性,我们提出一种基于 JWT+Redis 的混合存储方案。JWT(JSON Web Token)用于存储基础声明(如用户 ID、权限等),而 Redis 用于维护动态黑名单(如已注销的 token)。以下是实现的核心代码和逻辑:
1. JWT 生成与验证
使用 JJWT 库生成和验证 JWT,确保 token 的完整性和防篡改。以下是生成 JWT 的代码片段:
public String generateToken(UserDetails userDetails) {return Jwts.builder()
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();}
2. Redis 黑名单管理
使用 Redis 存储已注销的 token,并在验证时检查黑名单。以下是拦截器中的验证逻辑:
public boolean isTokenValid(String token) {if (redisTemplate.opsForValue().get("blacklist:" + token) != null) {return false;}
try {Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (Exception e) {return false;}
}
3. 自动续期逻辑
为避免频繁重新登录,可以在 token 接近过期时自动续期。以下是拦截器中的续期逻辑:
if (claims.getExpiration().getTime() - System.currentTimeMillis() < jwtRefreshMs) {String newToken = generateToken(userDetails);
response.setHeader("Authorization", "Bearer" + newToken);
}
性能验证
我们使用 JMeter 对三种存储方案进行了压测,测试环境为 8C16G/Redis 6.2。以下是测试结果:
- 内存存储 :QPS 高达 12 万,但重启后 token 丢失。
- Redis 集群 :QPS 稳定在 6 万左右,且支持持久化和集群同步。
- 数据库 :QPS 仅为 8000,延迟显著高于前两种方案。
此外,Redis 集群的分片策略对延迟也有影响。我们测试了哈希分片和范围分片,发现哈希分片在负载均衡和延迟方面表现更好。
安全实践
1. HTTPS 强制策略
为防止 Token 劫持,所有涉及 token 传输的接口必须强制使用 HTTPS。可以通过 Spring Security 配置:
http.requiresChannel()
.requestMatchers(r -> r.getHeader("Authorization") != null)
.requiresSecure();
2. 短期 Token 与 Refresh Token
短期 Token(如 1 小时过期)用于日常请求,Refresh Token(如 7 天过期)用于获取新的短期 Token。这样可以减少长期 Token 泄露的风险:
public TokenPair generateTokenPair(UserDetails userDetails) {String accessToken = generateToken(userDetails, jwtExpirationMs);
String refreshToken = generateToken(userDetails, refreshExpirationMs);
return new TokenPair(accessToken, refreshToken);
}
避坑指南
1. Redis 持久化导致 Token 意外失效
如果 Redis 配置了持久化(如 AOF),在重启时可能会因为持久化延迟导致 token 丢失。解决方案是使用 Redis 的主从复制或多副本集群,确保高可用性。
2. 分布式系统时钟偏差对 JWT 的影响
如果服务之间的时钟不同步,可能导致 JWT 验证失败。可以通过 NTP 服务同步时间,或在验证时允许一定的时钟偏差:
Jwts.parser()
.setAllowedClockSkewSeconds(30)
.setSigningKey(jwtSecret)
.parseClaimsJws(token);
开放问题
尽管我们的方案在大多数场景下表现良好,但在遭遇 Region 级 Redis 故障时,系统如何降级仍然是一个开放问题。可能的思路包括:
- 降级到本地内存缓存,牺牲一致性换取可用性。
- 使用多 Region 部署的 Redis 集群,实现异地容灾。
欢迎大家在评论区分享你的解决方案和经验!
