在现代 Web 应用中,身份认证是保障系统安全的核心环节。随着前后端分离架构的普及,JWT(JSON Web Token)凭借其无状态特性成为主流认证方案。而双 Token 机制则是在 JWT 基础上发展出的更安全、更灵活的认证模式。本文将结合实际代码实现,详细解析 JWT 双 Token 机制的工作原理、实现细节及安全考量。
什么是 JWT 双 Token 机制?
JWT 双 Token 机制通过同时使用两种令牌来实现身份认证:
- Access Token(访问令牌) :短期有效,主要用于 API 访问时的身份验证
- Refresh Token(刷新令牌) :长期有效,仅用于获取新的 Access Token
这种机制解决了单一令牌在安全性和用户体验之间的矛盾 —— 短期有效的 Access Token 降低了令牌泄露的风险,而长期有效的 Refresh Token 则避免了用户频繁登录的麻烦。
核心实现代码解析
1. 登录流程(Login API)
登录是获取双 Token 的入口,对应代码位于app/api/auth/login/route.ts:
// 验证用户凭据
const user = await prisma.user.findUnique({ where: { email } });
if(!user) {
return NextResponse.json({ error: 'Invalid credentials' }, { status: 401 });
}
// 验证密码
const isPasswordValid = await bcrypt.compare(password, user.password);
if(!isPasswordValid) {
return NextResponse.json({ error: 'Invalid password' }, { status: 401 });
}
// 生成双Token
const { accessToken, refreshToken } = await createTokens(user.id);
// 存储Refresh Token到数据库
await prisma.user.update({
where: { id: user.id },
data: { refreshToken }
});
// 设置认证Cookie
setAuthCookies(accessToken, refreshToken);
登录流程详解:
- 用户身份验证:验证邮箱格式、密码强度及正确性
- Token 生成:调用
createTokens()生成一对令牌 - 安全存储:将 Refresh Token 存入数据库,便于后续验证
- Cookie 设置:通过
setAuthCookies()方法将令牌存入 HTTP-only Cookie
2. Token 刷新流程(Refresh API)
当 Access Token 过期时,需要通过 Refresh Token 获取新的令牌对,对应代码位于app/api/auth/refresh/route.ts:
// 获取Cookie中的refresh token
const refreshToken = request.cookies.get("refresh_token")?.value;
// 验证refresh token存在性
if(!refreshToken) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 第一步:JWT签名验证
const refreshPayload = await verifyToken(refreshToken);
if(!refreshPayload || !refreshPayload.userId) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 第二步:数据库比对验证
const user = await prisma.user.findUnique({
where: { id: refreshPayload.userId as number }
});
if(!user || user.refreshToken !== refreshToken) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 生成新的双Token
const { accessToken: newAccessToken, refreshToken: newRefreshToken } = await createTokens(userId);
// 更新数据库中的refresh token
await prisma.user.update({
where: { id: userId },
data: { refreshToken: newRefreshToken }
});
// 设置新的Cookie
response.cookies.set('access_token', newAccessToken, {
httpOnly: true,
maxAge: 60*15, // 15分钟
sameSite: 'strict',
path: '/'
});
response.cookies.set('refresh_token', newRefreshToken, {
httpOnly: true,
maxAge: 60*60*24*7, // 7天
sameSite: 'strict',
path: '/'
});
刷新流程的双重验证机制:
- JWT 签名验证:确保令牌未被篡改且在有效期内
- 数据库比对验证:确保该 Refresh Token 是当前有效的令牌(防止已注销的令牌被滥用)
这种双重验证机制极大提高了系统安全性,即使 Refresh Token 被泄露,只要服务器端已更新令牌,攻击者也无法使用旧令牌获取新的 Access Token。
安全特性深度剖析
1. Cookie 安全设置
对 Cookie 的设置多层次的安全考量:
- httpOnly: true:禁止 JavaScript 访问 Cookie,有效防止 XSS(跨站脚本)攻击
- sameSite: 'strict' :限制 Cookie 仅在同站点请求中发送,有效防范 CSRF(跨站请求伪造)攻击
- 差异化过期时间:Access Token(15 分钟)远短于 Refresh Token(7 天),平衡安全性和用户体验
- path: '/' :确保 Cookie 在整个应用中可用
2. 令牌生命周期管理
- 短期 Access Token:减少令牌泄露后的风险窗口
- 定期刷新机制:每 15 分钟自动更新 Access Token
- Refresh Token 轮换:每次刷新都会生成新的 Refresh Token 并更新数据库,实现 "令牌滑动窗口"
3. 防御机制
- 双重验证:同时验证令牌签名和数据库存储状态
- 即时失效:通过删除数据库中的 Refresh Token 可立即撤销用户访问权限
- 格式验证:登录前验证邮箱和密码格式,减少无效请求
双 Token 机制工作流程图
用户 -> 登录请求 -> 服务器
|
| 1. 验证凭据
v
服务器 -> 生成双Token -> 存储Refresh Token -> 设置Cookie -> 用户
|
v
用户 -> API请求(带Access Token) -> 服务器
|
| 2. 验证Access Token
v
有效 -> 处理请求 -> 返回结果
无效 -> 401错误
|
v
用户 -> 刷新请求(带Refresh Token) -> 服务器
|
| 3. 双重验证Refresh Token
v
有效 -> 生成新双Token -> 更新存储和Cookie -> 用户
无效 -> 重定向到登录页
实际应用中的最佳实践
-
令牌存储策略:
- 始终使用 HTTP-only Cookie存储令牌,避免 localStorage 或 sessionStorage
- 生产环境中必须启用 HTTPS,防止 Cookie 在传输过程中被窃取
-
令牌过期时间设置:
- Access Token:15-30 分钟(根据业务敏感度调整)
- Refresh Token:7-30 天(根据用户体验需求调整)
-
前端实现建议:
- 拦截 401 响应,自动发起令牌刷新请求
- 实现刷新令牌失败后的优雅降级(重定向到登录页)
- 避免在前端存储令牌相关的敏感信息
总结
JWT 双 Token 机制通过 Access Token 和 Refresh Token 的协同工作,在安全性和用户体验之间取得了平衡。其核心优势在于:
- 安全性:短期有效的 Access Token 降低了泄露风险,双重验证机制防止滥用
- 灵活性:可随时撤销特定用户的访问权限,无需维护全局黑名单
- 用户体验:减少登录频率,同时保持较高的安全标准
- 可扩展性:便于支持多设备登录、单点登录等复杂场景
通过本文解析的代码实现,我们可以看到双 Token 机制如何在实际项目中落地。在实际应用中,还需要根据具体业务需求和安全级别,调整令牌有效期、验证策略和防御措施,构建更适合自身系统的认证方案。