4. Redis 与 JWT 集成架构设计
在 SaaS 鉴权链路中,JWT 作为无状态令牌承载用户身份与权限声明,Redis 则作为高性能缓存层承载令牌的校验、黑名单、刷新与分布式会话状态。本章从整体架构图出发,明确 Token 缓存层的定位,并系统阐述缓存穿透、击穿、雪崩三大风险的防护思路。
4.1 整体架构图:SaaS 鉴权拦截链路缓存层
下图展示了从客户端请求到后端服务的完整鉴权链路,Redis 作为核心缓存层位于网关与业务服务之间,承担 Token 校验、黑名单、刷新令牌与限流计数等职责。
4.2 Token 缓存层定位
Token 缓存层并非简单的键值存储,而是 SaaS 鉴权体系中的“状态决策中心”。其核心定位体现在以下三个维度:
| 维度 | 定位说明 | 典型 Key 设计 |
|---|---|---|
| 校验加速 | 缓存 JWT 签名校验结果与用户权限快照,避免每次请求都解密 JWT 或查询数据库 | auth:token:{jti}:valid |
| 状态管理 | 维护 Token 黑名单、刷新令牌、设备登录状态等动态数据 | auth:blacklist:{jti} |
| 限流与熔断 | 基于 Redis 计数器和滑动窗口实现接口级限流,保护下游服务 | auth:ratelimit:{userId}:{api} |
设计原则:Token 缓存层应遵循“读多写少、最终一致、自动过期”原则。JWT 本身无状态,Redis 缓存层为其补充了必要的运行时状态,但不可将 Redis 作为持久化存储,所有缓存数据均应设置 TTL。
4.3 缓存穿透防护思路
缓存穿透指请求查询一个根本不存在的数据(如伪造的 JWT),导致每次请求都穿透 Redis 直达数据库,造成数据库压力激增。
4.3.1 防护策略对比
| 策略 | 实现方式 | 适用场景 | 注意事项 |
|---|---|---|---|
| 空值缓存 | 查询结果为空时,在 Redis 中缓存一个短 TTL 的空标记 | Token 黑名单、用户不存在等场景 | TTL 不宜过长(建议 30-60 秒),防止数据已存在但缓存仍为空 |
| 布隆过滤器 | 在网关层前置布隆过滤器,快速判断 Token 是否可能存在 | 大规模 SaaS 租户场景,Token 总量百万级以上 | 存在误判率,需结合空值缓存兜底 |
| 参数校验 | 在进入缓存层前对 JWT 格式、签名、过期时间做基础校验 | 所有请求必经之路 | 不能完全替代缓存层防护,但可过滤大量恶意请求 |
4.3.2 空值缓存代码示例
// 查询 Token 校验结果
public Boolean checkTokenValid(String jti) {
String key = "auth:token:" + jti + ":valid";
String cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
return "true".equals(cached);
}
// 回源校验 JWT 签名与黑名单
Boolean valid = doRealValidation(jti);
// 无论是否有效都写入缓存,无效时设置短 TTL
if (valid) {
redisTemplate.opsForValue().set(key, "true", 300, TimeUnit.SECONDS);
} else {
redisTemplate.opsForValue().set(key, "false", 30, TimeUnit.SECONDS);
}
return valid;
}
4.4 缓存击穿防护思路
缓存击穿指一个热点 Token(如超级管理员 Token)在缓存过期瞬间,大量并发请求同时回源查询数据库,导致数据库瞬时压力飙升。
4.4.1 防护策略
- 互斥锁(Mutex Key):当缓存失效时,只允许一个线程回源加载数据,其他线程等待或快速失败。
- 逻辑过期:缓存数据永不过期,但数据中携带逻辑过期时间,异步线程负责刷新。
- 热点预加载:针对已知热点 Token(如 SaaS 平台内置管理员),在缓存即将过期前主动刷新。
4.4.2 互斥锁实现示例
public String getTokenWithMutex(String jti) {
String key = "auth:token:" + jti + ":valid";
String value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 尝试获取分布式锁
String lockKey = "lock:token:" + jti;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 5, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 双重检查,防止在获取锁期间缓存已被其他线程重建
value = redisTemplate.opsForValue().get(key);
if (value != null) {
return value;
}
// 回源加载
value = loadFromDatabase(jti);
redisTemplate.opsForValue().set(key, value, 300, TimeUnit.SECONDS);
return value;
} finally {
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,等待后重试或返回降级数据
Thread.sleep(50);
return getTokenWithMutex(jti); // 递归重试
}
}
4.5 缓存雪崩防护思路
缓存雪崩指大量缓存数据在同一时间过期,或 Redis 节点宕机,导致所有请求直接压向数据库,引发连锁故障。
4.5.1 防护策略
| 策略 | 实现方式 | 效果 |
|---|---|---|
| 过期时间随机化 | 在基础 TTL 上增加随机偏移(±30%) | 避免大量 Key 同时过期 |
| 多级缓存 | 本地缓存(Caffeine)+ Redis 缓存组合 | Redis 宕机时本地缓存仍可提供服务 |
| Redis 高可用 | 主从 + Sentinel 或 Redis Cluster | 单点故障自动切换 |
| 限流降级 | 网关层对回源请求实施限流,超出阈值直接返回降级响应 | 保护数据库不被冲垮 |
4.5.2 过期时间随机化代码示例
// 生成随机 TTL,基础 300 秒,随机偏移 ±30%
public long getRandomTtl(long baseTtl) {
double factor = 0.7 + Math.random() * 0.6; // 0.7 ~ 1.3
return (long) (baseTtl * factor);
}
// 设置缓存时使用随机 TTL
public void setTokenCache(String jti, String value) {
String key = "auth:token:" + jti + ":valid";
long ttl = getRandomTtl(300);
redisTemplate.opsForValue().set(key, value, ttl, TimeUnit.SECONDS);
}
4.6 三大风险防护思路总结
重要提醒:缓存穿透、击穿、雪崩三种风险可能同时发生。例如,Redis 节点宕机(雪崩)期间,大量恶意请求携带伪造 JWT(穿透)同时回源,若未实施限流降级,数据库将瞬间崩溃。因此,防护策略必须分层组合,形成纵深防御体系。