正在加载,请稍候…

JWT 刷新令牌轮换:带盗窃检测的安全认证

实现带刷新令牌轮换的 JWT 认证——令牌族、通过重用检测盗窃、httpOnly Cookie 和撤销策略。

JWT 刷新令牌轮换:带盗窃检测的安全认证

访问令牌 + 刷新令牌架构

访问令牌 15 分钟过期。刷新令牌存活 7 天,但每次使用后轮换。

JWT 刷新令牌轮换:带盗窃检测的安全认证示意图

令牌生成

export function generateTokens(userId: string) {
  const accessToken = jwt.sign(
    { sub: userId, type: 'access' },
    process.env.JWT_ACCESS_SECRET!,
    { expiresIn: '15m' }
  )
  const refreshToken = jwt.sign(
    { sub: userId, type: 'refresh', jti: randomBytes(16).toString('hex') },
    process.env.JWT_REFRESH_SECRET!,
    { expiresIn: '7d' }
  )
  return { accessToken, refreshToken }
}

JWT 刷新令牌轮换:带盗窃检测的安全认证示意图

带盗窃检测的轮换

app.post('/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies

  try {
    const payload = verifyRefreshToken(refreshToken)

    // 令牌已被使用?= 检测到盗窃
    const stored = await db.refreshToken.findFirst({ where: { jti: payload.jti } })
    if (!stored) {
      await db.refreshToken.deleteMany({ where: { userId: payload.sub } })
      return res.status(401).json({ error: 'Token reuse detected' })
    }

    await db.refreshToken.delete({ where: { jti: payload.jti } })

    const tokens = generateTokens(payload.sub)
    const newPayload = verifyRefreshToken(tokens.refreshToken)

    await db.refreshToken.create({
      data: { jti: newPayload.jti, userId: payload.sub, expiresAt: new Date(Date.now() + 7 * 86400_000) }
    })

    res.cookie('refreshToken', tokens.refreshToken, { httpOnly: true, secure: true, sameSite: 'strict' })
    return res.json({ accessToken: tokens.accessToken })
  } catch {
    return res.status(401).json({ error: 'Invalid token' })
  }
})

JWT 刷新令牌轮换:带盗窃检测的安全认证示意图

存储最佳实践

位置 访问令牌 刷新令牌
内存 最佳 页面刷新后丢失
httpOnly Cookie 良好 最佳
localStorage XSS 风险 绝不

-> 使用 JWT Parser 解析 JWT 令牌。