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

令牌生成
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 }
}

带盗窃检测的轮换
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' })
}
})

存储最佳实践
| 位置 | 访问令牌 | 刷新令牌 |
|---|---|---|
| 内存 | 最佳 | 页面刷新后丢失 |
| httpOnly Cookie | 良好 | 最佳 |
| localStorage | XSS 风险 | 绝不 |
-> 使用 JWT Parser 解析 JWT 令牌。