侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

设计安全的JWT认证与刷新机制并处理并发请求

2025-12-12 / 0 评论 / 4 阅读

题目

设计安全的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时,通过版本号声明实现快速撤销检查