Skip to content

深入理解 Token 认证与无感刷新机制 ​

什么是双 Token 机制?🤔 ​

在现代 Web 应用中,Token 认证机制扮演着至关重要的角色。但为什么我们需要使用双 Token 机制呢?

Access Token 和 Refresh Token 的区别 ​

  • Access Token 🎫

    • 用于访问受保护的资源
    • 有效期较短(通常几小时或更短)
    • 频繁使用
    • 安全要求高
  • Refresh Token 🔄

    • 用于获取新的 Access Token
    • 有效期较长(通常几天到几周)
    • 使用频率低
    • 只用于刷新,不能直接访问资源

认证流程 📝 ​

  1. 用户登录 → 服务器返回 Access Token 和 Refresh Token
  2. 使用 Access Token 请求资源
  3. Access Token 过期 → 服务器返回过期错误
  4. 使用 Refresh Token 获取新的 Access Token
  5. 使用新的 Access Token 继续访问资源

无感刷新实现方案 ⚙️ ​

方案一:请求前拦截 ​

javascript
// 请求前判断 token 是否过期
if (isTokenExpired()) {
  await refreshToken();
}
// 继续发起原请求

优点 ✅

  • 可以减少无效请求
  • 提高性能和用户体验

缺点 ❌

  • 需要后端提供 token 过期时间
  • 依赖客户端时间,可能存在安全隐患

方案二:响应后拦截 ​

javascript
// 响应拦截器中处理
if (response.code === TOKEN_EXPIRED) {
  await refreshToken();
  return retry(originalRequest);
}

优点 ✅

  • 实现简单
  • 不依赖客户端时间
  • 更可靠

缺点 ❌

  • 会产生额外的请求开销

最佳实践示例 💡 ​

1. 配置常量 ​

javascript
// constants.js
export const TOKEN_CONFIG = {
  ACCESS_TOKEN: 'access_token',
  REFRESH_TOKEN: 'refresh_token',
  TOKEN_HEADER: 'Authorization',
  REFRESH_HEADER: 'X-Refresh-Token'
};

2. 请求拦截器实现 ​

javascript
// axios 配置
const service = axios.create({
  timeout: 30000,
  baseURL: process.env.API_BASE_URL
});

// 请求队列管理
let refreshing = false;
const waitQueue = [];

service.interceptors.response.use(
  response => response,
  async error => {
    const { config, response } = error;
    
    if (response?.status === 401 && !config._retry) {
      if (refreshing) {
        // 将请求加入等待队列
        return new Promise(resolve => {
          waitQueue.push(() => resolve(service(config)));
        });
      }
      
      refreshing = true;
      config._retry = true;
      
      try {
        const newToken = await refreshAccessToken();
        // 更新 token 后重试所有等待的请求
        waitQueue.forEach(cb => cb());
        waitQueue.length = 0;
        return service(config);
      } catch (err) {
        // 刷新失败,跳转登录
        router.push('/login');
        return Promise.reject(err);
      } finally {
        refreshing = false;
      }
    }
    
    return Promise.reject(error);
  }
);

注意事项 ⚠️ ​

  1. 安全性考虑

    • Refresh Token 应妥善保存
    • 建议使用 HttpOnly Cookie 存储
    • 考虑使用 HTTPS 传输
  2. 并发请求处理

    • 多个请求同时失效时要避免重复刷新
    • 使用请求队列管理并发
  3. 异常处理

    • 刷新失败时要有合适的降级策略
    • 考虑网络异常、服务器异常等场景
  4. 用户体验

    • 刷新过程对用户应该是无感知的
    • 可以添加适当的加载提示

参考资料 📚 ​