JWT Token 无感刷新机制:现代Web应用的安全认证解决方案
引言
在现代Web应用中,用户认证是一个核心功能。传统的单Token方案虽然简单,但存在安全风险:长期存储的Token容易被盗用,而短期Token又会导致用户频繁登录,影响用户体验。JWT双Token无感刷新机制完美解决了这一矛盾,既保证了安全性,又提供了流畅的用户体验。
本文将通过一个基于Next.js的完整项目,深入解析JWT无感刷新的实现原理和最佳实践。
1. 双Token机制概述
1.1 核心概念
JWT双Token机制包含两个令牌:
- AccessToken(访问令牌):短期令牌(15分钟),用于API访问授权
- RefreshToken(刷新令牌):长期令牌(7天),用于刷新AccessToken
1.2 安全优势
// 访问令牌 - 短期,降低被盗用风险
const accessToken = await new SignJWT({userId})
.setProtectedHeader({alg: 'HS256'})
.setIssuedAt()
.setExpirationTime('15min') // 15分钟过期
.sign(getJwtSecretKey());
// 刷新令牌 - 长期,用于无感刷新
const refreshToken = await new SignJWT({userId})
.setProtectedHeader({alg: 'HS256'})
.setIssuedAt()
.setExpirationTime('7d') // 7天过期
.sign(getJwtSecretKey());
2. 无感刷新实现原理
2.1 整体架构
无感刷新机制通过以下组件协同工作:
- 中间件(Middleware):拦截请求,验证Token
- 刷新接口(Refresh API):处理Token刷新逻辑
- JWT工具库:Token生成和验证
- 数据库:存储RefreshToken
2.2 核心流程
sequenceDiagram
participant U as 用户浏览器
participant M as 中间件
participant R as 刷新接口
participant D as 数据库
U->>M: 访问 /dashboard
M->>M: 检查 AccessToken
M-->>M: AccessToken 过期
M->>M: 检查 RefreshToken
M->>R: 重定向到刷新接口
R->>R: 验证 RefreshToken
R->>D: 查询用户信息
R->>R: 生成新双Token
R->>D: 更新 RefreshToken
R->>U: 设置新Cookie + 重定向
U->>M: 重新访问 /dashboard
M->>M: 验证新 AccessToken
M->>U: 正常访问页面
3. 核心代码实现
3.1 中间件实现
中间件是整个无感刷新机制的核心,负责拦截请求并处理Token验证:
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
// 检查是否为受保护路径
if (!protectedRoutes.some((p) => path.startsWith(p))) {
return NextResponse.next();
}
const refreshToken = request.cookies.get("refresh_token")?.value;
const accessToken = request.cookies.get("access_token")?.value;
// 1. 优先验证 AccessToken
if (accessToken) {
const accessPayload = await verifyToken(accessToken);
if (accessPayload) {
// Token有效,添加用户信息到请求头
const requestHeaders = new Headers(request.headers);
requestHeaders.set("x-user-id", accessPayload.userId as string);
return NextResponse.next({ request: { headers: requestHeaders } });
}
}
// 2. AccessToken无效,尝试使用RefreshToken刷新
if (refreshToken) {
const refreshPayload = await verifyToken(refreshToken);
if (refreshPayload) {
// 重定向到刷新接口
const refreshUrl = new URL("/api/auth/refresh", request.url);
refreshUrl.searchParams.set("redirect", request.url);
return NextResponse.redirect(refreshUrl);
}
}
// 3. 所有Token都无效,重定向到登录页
return NextResponse.redirect(new URL("/login", request.url));
}
3.2 刷新接口实现
刷新接口负责生成新Token并更新存储:
export async function GET(request: NextRequest) {
try {
const refreshToken = request.cookies.get("refresh")?.value;
const redirectUrl = request.nextUrl.searchParams.get("redirect") || "/dashboard";
// 1. 验证RefreshToken
if (!refreshToken) {
return NextResponse.redirect(new URL("/login", request.url));
}
const refreshPayload = await verifyToken(refreshToken);
if (!refreshPayload || !refreshPayload.userId) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 2. 数据库二次验证(安全加固)
const user = await prisma.user.findUnique({
where: { id: refreshPayload.userId }
});
if (!user || user.refreshToken !== refreshToken) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 3. 生成新的双Token
const { accessToken: newAccessToken, refreshToken: newRefreshToken } =
await createTokens(user.id);
// 4. 更新数据库中的RefreshToken(令牌轮换)
await prisma.user.update({
where: { id: user.id },
data: { refreshToken: newRefreshToken }
});
// 5. 设置新Cookie并重定向
const response = NextResponse.redirect(new URL(redirectUrl, request.url));
response.cookies.set('accessToken', newAccessToken, {
httpOnly: true,
maxAge: 60 * 15,
sameSite: 'strict',
path: '/'
});
response.cookies.set('refreshToken', newRefreshToken, {
httpOnly: true,
maxAge: 60 * 60 * 24 * 7,
sameSite: 'strict',
path: '/'
});
return response;
} catch(error) {
console.error('Token refresh error:', error);
return NextResponse.redirect(new URL('/login', request.url));
}
}
3.3 JWT工具库
JWT工具库提供Token的创建、验证和Cookie设置功能:
// 创建双Token
export const createTokens = async (userId: number) => {
const accessToken = await new SignJWT({userId})
.setProtectedHeader({alg: 'HS256'})
.setIssuedAt()
.setExpirationTime('15min')
.sign(getJwtSecretKey());
const refreshToken = await new SignJWT({userId})
.setProtectedHeader({alg: 'HS256'})
.setIssuedAt()
.setExpirationTime('7d')
.sign(getJwtSecretKey());
return { accessToken, refreshToken };
};
// 设置安全Cookie
export const setAuthCookies = async (accessToken: string, refreshToken: string) => {
const cookieStore = await cookies();
cookieStore.set('accessToken', accessToken, {
httpOnly: true, // 防止XSS攻击
maxAge: 15 * 60,
sameSite: 'strict', // 防止CSRF攻击
path: '/'
});
cookieStore.set('refreshToken', refreshToken, {
httpOnly: true,
maxAge: 7 * 24 * 60 * 60,
sameSite: 'strict',
path: '/'
});
};
4. 安全特性分析
4.1 多层安全防护
- JWT签名验证:使用HS256算法确保Token完整性
- 数据库二次验证:防止Token重放攻击
- 令牌轮换机制:每次刷新都生成新的RefreshToken
- HttpOnly Cookie:防止XSS攻击
- SameSite设置:防止CSRF攻击
4.2 令牌生命周期管理
// 访问令牌:15分钟生命周期
.setExpirationTime('15min')
// 刷新令牌:7天生命周期
.setExpirationTime('7d')
// Cookie过期时间与Token保持一致
maxAge: 15 * 60, // AccessToken Cookie
maxAge: 7 * 24 * 60 * 60, // RefreshToken Cookie
5. 用户体验优化
5.1 无感知刷新
用户在整个刷新过程中:
- 不会看到登录页面
- 不需要手动重新输入密码
- 页面访问流程完全透明
5.2 错误处理
// 优雅的错误处理
try {
// 刷新逻辑
} catch(error) {
console.error('Token refresh error:', error);
return NextResponse.redirect(new URL('/login', request.url));
}
6. 最佳实践建议
6.1 安全建议
- 环境变量管理:JWT密钥必须存储在环境变量中
- HTTPS部署:生产环境必须使用HTTPS
- 定期密钥轮换:定期更换JWT签名密钥
- 监控异常:记录Token刷新失败的情况
6.2 性能优化
- 数据库连接池:合理配置Prisma连接池
- 缓存策略:对频繁访问的用户信息进行缓存
- 异步处理:使用异步操作避免阻塞
6.3 代码质量
- 类型安全:使用TypeScript确保类型安全
- 错误处理:完善的错误处理和日志记录
- 代码注释:详细的代码注释和文档
7. 总结
JWT双Token无感刷新机制是现代Web应用认证的最佳实践之一。通过合理的Token生命周期设计、多层安全防护和优雅的用户体验,既保证了应用的安全性,又提供了流畅的用户交互。
本文通过一个完整的Next.js项目展示了无感刷新机制的实现细节,包括中间件拦截、Token验证、数据库操作和Cookie管理等核心功能。开发者可以根据实际需求调整Token过期时间、安全策略和错误处理逻辑,构建适合自己项目的认证系统。
随着Web应用安全要求的不断提高,无感刷新机制将成为现代Web开发的标准配置,为用户提供既安全又便捷的认证体验。