题目
设计安全的JWT认证与刷新机制,并处理并发请求中的令牌刷新问题
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
JWT安全机制, 刷新令牌设计, 并发控制, 防重放攻击, 分布式系统实现
快速回答
核心解决方案要点:
- 采用双令牌机制:短有效期访问令牌(15-30分钟)+ 长有效期刷新令牌(7天)
- 刷新令牌需安全存储(HttpOnly Cookie),访问令牌存客户端内存
- 处理并发刷新时:使用互斥锁(Redis SETNX)或请求队列防止重复刷新
- JWT需包含jti(唯一标识)并维护短期黑名单防重放
- 强制签名算法(HS256/RS256),禁用none算法
1. 核心原理说明
JWT认证的安全隐患主要来自令牌劫持和过期处理:
- 双令牌机制原理:访问令牌(Access Token)包含基础声明(sub, exp等),刷新令牌(Refresh Token)是服务端生成的随机字符串,两者关联存储
- 并发刷新问题:当多个并发请求同时检测到令牌过期时,可能触发多次刷新请求,导致令牌冲突或系统过载
- 安全加固:通过jti(JWT ID)唯一标识、签名验证、算法白名单防止篡改
2. 代码实现示例
令牌生成(Node.js):
// 生成访问令牌
const accessToken = jwt.sign(
{
userId: 'user123',
jti: crypto.randomUUID(), // 唯一标识
role: 'admin'
},
process.env.JWT_SECRET,
{ expiresIn: '15m', algorithm: 'HS256' } // 强制算法
);
// 生成刷新令牌(服务端存储)
const refreshToken = crypto.randomBytes(64).toString('hex');
redis.set(`refresh:${userId}`, refreshToken, 'EX', 604800); // 7天过期并发刷新处理(伪代码):
async function refreshAccessToken(userId, oldRefreshToken) {
// 获取Redis互斥锁
const lockKey = `lock:refresh:${userId}`;
const lock = await redis.set(lockKey, '1', 'EX', 5, 'NX');
if (!lock) throw new Error('Refresh in progress');
try {
// 验证刷新令牌
const storedToken = await redis.get(`refresh:${userId}`);
if (storedToken !== oldRefreshToken) throw new Error('Invalid token');
// 生成新令牌并加入黑名单(旧访问令牌)
const newAccessToken = generateToken(userId);
await redis.set(`jti:${oldJti}`, 'revoked', 'EX', 900); // 黑名单15分钟
return newAccessToken;
} finally {
await redis.del(lockKey); // 释放锁
}
}3. 最佳实践
- 存储策略:访问令牌存前端内存(非localStorage),刷新令牌用HttpOnly + Secure Cookie
- 刷新流程:客户端通过专用/refresh端点刷新,返回新访问令牌但刷新令牌不变
- 过期处理:前端拦截401响应,队列化刷新请求,其他请求暂停直至刷新完成
- 密钥管理:HS256使用32字节以上密钥,RS256定期轮换私钥
4. 常见错误与风险
- 并发问题:未加锁导致重复刷新,引发令牌不一致或竞争条件
- 安全漏洞:JWT未验证签名算法(可能被篡改为none)或缺失jti导致重放攻击
- 令牌泄露:刷新令牌存localStorage易受XSS攻击
- 分布式陷阱:黑名单未同步到所有节点,导致部分服务失效
5. 扩展知识
- 无状态进阶方案:使用短效令牌+密钥轮换替代黑名单(如AWS JWT方案)
- OAuth2.0关联:刷新令牌机制需符合RFC6749标准,需实现令牌吊销端点
- 性能优化:用jwt.decode获取过期时间,避免过早刷新
- 量子安全准备:未来可迁移至抗量子签名算法(如EdDSA)