告别频繁登录!Next.js 实战 JWT 双 Token 无感刷新,安全与体验兼得

128 阅读5分钟

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 整体架构

无感刷新机制通过以下组件协同工作:

  1. 中间件(Middleware):拦截请求,验证Token
  2. 刷新接口(Refresh API):处理Token刷新逻辑
  3. JWT工具库:Token生成和验证
  4. 数据库:存储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 多层安全防护

  1. JWT签名验证:使用HS256算法确保Token完整性
  2. 数据库二次验证:防止Token重放攻击
  3. 令牌轮换机制:每次刷新都生成新的RefreshToken
  4. HttpOnly Cookie:防止XSS攻击
  5. 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 安全建议

  1. 环境变量管理:JWT密钥必须存储在环境变量中
  2. HTTPS部署:生产环境必须使用HTTPS
  3. 定期密钥轮换:定期更换JWT签名密钥
  4. 监控异常:记录Token刷新失败的情况

6.2 性能优化

  1. 数据库连接池:合理配置Prisma连接池
  2. 缓存策略:对频繁访问的用户信息进行缓存
  3. 异步处理:使用异步操作避免阻塞

6.3 代码质量

  1. 类型安全:使用TypeScript确保类型安全
  2. 错误处理:完善的错误处理和日志记录
  3. 代码注释:详细的代码注释和文档

7. 总结

JWT双Token无感刷新机制是现代Web应用认证的最佳实践之一。通过合理的Token生命周期设计、多层安全防护和优雅的用户体验,既保证了应用的安全性,又提供了流畅的用户交互。

本文通过一个完整的Next.js项目展示了无感刷新机制的实现细节,包括中间件拦截、Token验证、数据库操作和Cookie管理等核心功能。开发者可以根据实际需求调整Token过期时间、安全策略和错误处理逻辑,构建适合自己项目的认证系统。

随着Web应用安全要求的不断提高,无感刷新机制将成为现代Web开发的标准配置,为用户提供既安全又便捷的认证体验。