题目
设计安全的JWT认证与刷新机制并处理并发请求
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
JWT原理,刷新令牌机制,并发请求处理,安全性设计
快速回答
核心解决方案要点:
- 采用双令牌机制:短效访问令牌(15-30分钟) + 长效刷新令牌(7天)
- 刷新令牌存储于HttpOnly Cookie中,访问令牌通过Authorization Header传递
- 实现原子性刷新操作:使用Redis分布式锁+令牌版本号控制
- 刷新令牌一次性使用,刷新后立即失效并生成新令牌
- 处理并发请求时:
- 首个请求获取锁执行刷新
- 后续请求等待刷新结果
- 返回新访问令牌给所有等待请求
- 强制HTTPS传输,启用SameSite和Secure标志
1. 原理说明
JWT认证的核心挑战在于平衡安全性与用户体验:
- 双令牌机制:访问令牌(Access Token)包含用户声明(如用户ID、角色),有效期短(15-30分钟),减少泄露风险;刷新令牌(Refresh Token)仅用于获取新访问令牌,存储于安全Cookie
- 并发问题本质:当多个请求同时检测到令牌过期并尝试刷新时,可能造成:
- 刷新令牌被多次使用(违反一次性原则)
- 产生多个有效令牌集
- 竞争条件导致逻辑错误
- 安全基础:JWT签名验证(推荐RS256非对称加密),刷新令牌必须不可预测且绑定设备/IP
2. 完整解决方案(Node.js示例)
2.1 令牌生成
// 生成访问令牌(使用RS256非对称加密)
function generateAccessToken(userId, version) {
return jwt.sign(
{ sub: userId, exp: Math.floor(Date.now()/1000) + 900, v: version },
privateKey,
{ algorithm: 'RS256' }
);
}
// 生成刷新令牌(使用高强度随机数)
function generateRefreshToken() {
return crypto.randomBytes(64).toString('hex');
}2.2 刷新端点实现(核心逻辑)
app.post('/refresh', async (req, res) => {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) return res.sendStatus(401);
// 1. 验证刷新令牌有效性
const userSession = await db.findSession(refreshToken);
if (!userSession || userSession.revoked) return res.sendStatus(401);
// 2. 获取分布式锁(Redis实现)
const lockKey = `refresh_lock:${userSession.userId}`;
const lock = await redis.set(lockKey, 'LOCKED', 'EX', 5, 'NX');
if (!lock) {
// 3. 已有刷新操作在进行,等待结果
return waitForRefreshCompletion(userSession.userId, res);
}
try {
// 4. 原子性刷新操作
const newVersion = userSession.tokenVersion + 1;
const newAccessToken = generateAccessToken(userSession.userId, newVersion);
const newRefreshToken = generateRefreshToken();
// 5. 更新数据库(事务操作)
await db.transaction(async (tx) => {
await tx.updateSession(userSession.id, { revoked: true }); // 使旧刷新令牌失效
await tx.createSession({
userId: userSession.userId,
refreshToken: newRefreshToken,
tokenVersion: newVersion
});
});
// 6. 设置新Cookie并返回
res.cookie('refreshToken', newRefreshToken, {
httpOnly: true,
secure: true,
sameSite: 'Strict',
maxAge: 604800000 // 7天
});
res.json({ accessToken: newAccessToken });
// 7. 通知等待中的请求
notifyPendingRequests(userSession.userId, newAccessToken);
} finally {
await redis.del(lockKey); // 释放锁
}
});
// 等待刷新完成的请求处理
async function waitForRefreshCompletion(userId, res) {
return new Promise((resolve) => {
const listener = (newToken) => {
res.json({ accessToken: newToken });
resolve();
};
eventEmitter.once(`refresh:${userId}`, listener);
setTimeout(() => {
eventEmitter.off(`refresh:${userId}`, listener);
res.status(408).send('Refresh timeout');
resolve();
}, 5000); // 5秒超时
});
}3. 最佳实践
- 令牌存储:访问令牌存内存或SessionStorage,刷新令牌必须HttpOnly Cookie
- 版本控制:每个刷新令牌关联版本号,旧版本立即失效
- 安全传输:强制HTTPS,Cookie设置Secure和SameSite=Strict
- 密钥管理:私钥仅签名服务可用,公钥分发至验证服务
- 监控日志:记录异常刷新行为(如频繁刷新、多地刷新)
4. 常见错误
- 并发处理缺失:未加锁导致多次刷新(严重安全漏洞)
- 令牌存储不当:刷新令牌存localStorage(XSS风险)
- 无版本控制:允许旧刷新令牌继续使用
- 过长有效期:访问令牌超过1小时增加风险
- 日志泄露敏感信息:在日志中打印完整JWT
- 无撤销机制:用户退出后令牌仍有效
5. 扩展知识
- JWT黑名单:对于未过期的访问令牌,可用Redis记录撤销列表(jti声明)
- 设备绑定:刷新令牌关联设备指纹,异常设备需重新认证
- OAuth 2.0扩展:PKCE(Proof Key for Code Exchange)防止授权码截获
- 替代方案:Biscuit令牌(支持离线撤销)、PASETO(更安全的JWT替代)
- 性能优化:使用无状态JWT时,通过版本号声明实现快速撤销检查