题目
设计安全的JWT认证系统并防御令牌泄露和重放攻击
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
JWT安全机制,令牌刷新策略,重放攻击防御,存储安全
快速回答
实现安全JWT系统的核心要点:
- 使用短期访问令牌(15-30分钟)和长期刷新令牌(7天)分离机制
- 刷新令牌存储于HttpOnly Secure Cookie,访问令牌存客户端内存
- 实现刷新令牌轮换(每次刷新签发新令牌/撤销旧令牌)
- 添加JWT唯一标识符(jti)和服务端令牌黑名单
- 强制HTTPS传输并绑定令牌到用户设备指纹
一、核心安全挑战
JWT常见安全风险:
- 令牌泄露:XSS攻击或不当存储导致令牌被盗
- 重放攻击:拦截有效令牌重复使用
- 无法立即失效:JWT天然无状态导致注销困难
二、完整解决方案设计
1. 双令牌机制实现
// 生成令牌示例(Node.js)
const accessToken = jwt.sign(
{ userId: 'u123', jti: crypto.randomUUID() },
process.env.SECRET,
{ expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ userId: 'u123', deviceId: 'device_fingerprint' },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' }
);2. 安全存储策略
- 访问令牌:仅存客户端内存(不持久化)
- 刷新令牌:通过HttpOnly Secure Cookie传输(SameSite=Strict)
- 服务端:记录刷新令牌与设备/用户的绑定关系
3. 刷新令牌轮换流程
# 伪代码示例(Python)
def refresh_token(old_refresh_token):
# 验证令牌有效性
payload = verify(old_refresh_token, REFRESH_SECRET)
# 检查令牌是否已被使用
if redis.get(f"revoked:{payload['jti']}"):
abort(401, "Token revoked")
# 生成新令牌并撤销旧令牌
new_refresh_token = generate_token(payload['userId'])
redis.setex(f"revoked:{payload['jti']}", 7*24*3600, "revoked")
return {
"access_token": new_access_token(),
"refresh_token": new_refresh_token
}4. 重放攻击防御
- 在JWT声明中添加唯一标识符
jti - 服务端维护短期令牌黑名单(Redis实现):
redis.setex("blacklist:"+jti, 15*60, "blocked") - 校验请求时检查黑名单:
if redis.exists("blacklist:"+decoded.jti): return 401
三、最佳实践
- 密钥管理:使用HS256或RS256算法,定期轮换密钥
- 声明最小化:JWT payload仅包含必要信息
- 设备绑定:在刷新令牌中嵌入设备指纹(如IP+UserAgent哈希)
- 监控:记录异常刷新频率(如1分钟内多次刷新)
四、常见错误
- ❌ 将访问令牌存localStorage(易受XSS攻击)
- ❌ 使用单一长期令牌(无刷新机制)
- ❌ 未实现jti黑名单导致无法即时注销
- ❌ 未绑定设备信息允许令牌跨设备使用
五、扩展知识
- OAuth 2.1:推荐PKCE扩展防御授权码拦截
- Token Binding:将令牌与TLS会话绑定(RFC 8471)
- 分布式黑名单:使用Redis Cluster跨服务同步状态
- JWT替代方案:PASETO(更安全的令牌标准)