基于本文回答

播面 播面

刷题像听歌,多听自然懂
0
评论

前端如何检测和处理过期的 Token?

知识点图片

前端检测和处理过期 Token 是保障用户体验和应用安全的关键环节。通常有被动处理(响应拦截)主动处理(请求前检查)两种主要策略,实际项目中通常结合使用。

以下是详细的解决方案和代码示例(以 Axios 为例):


1. 核心概念:双 Token 机制

在讨论处理之前,通常预设后端采用了 双 Token 机制

  • Access Token: 短期有效(如 15 分钟),用于请求资源。
  • Refresh Token: 长期有效(如 7 天),用于换取新的 Access Token。

2. 策略一:被动处理(响应拦截器)

这是最常用的方式。前端正常发送请求,当后端返回 401 Unauthorized 时,前端拦截该错误,尝试刷新 Token 并重发原请求。

难点处理:并发请求
当页面同时发出 5 个请求且 Token 过期时,会瞬间收到 5 个 401。如果处理不当,会触发 5 次刷新 Token 接口,导致资源浪费或逻辑错误。必须引入“请求锁”和“重试队列”。

代码实现(Axios 拦截器):

javascript
import axios from 'axios';

// 是否正在刷新 Token 的标记
let isRefreshing = false;
// 重试队列,每一项是一个函数
let requestsQueue = [];

const instance = axios.create({
  baseURL: '/api',
});

// 响应拦截器
instance.interceptors.response.use(
  (response) => response,
  async (error) => {
    const { config, response } = error;

    // 如果没有响应(网络错误等),直接抛出
    if (!response) return Promise.reject(error);

    // 检测是否是 Token 过期 (通常状态码为 401)
    // 注意:有些后端可能返回 200 但在 body 里标记 code: 401,需根据实际调整
    if (response.status === 401 && !config._retry) {
      
      // 如果正在刷新,将当前请求放入队列
      if (isRefreshing) {
        return new Promise((resolve) => {
          requestsQueue.push((newToken) => {
            config.headers['Authorization'] = `Bearer ${newToken}`;
            resolve(instance(config)); // 重新发送请求
          });
        });
      }

      // 开启刷新锁
      config._retry = true; // 标记该请求已重试过,防止死循环
      isRefreshing = true;

      try {
        // 1. 调用刷新 Token 接口 (携带 Refresh Token)
        const { data } = await axios.post('/auth/refresh-token', {
          refreshToken: localStorage.getItem('refreshToken'),
        });

        const newAccessToken = data.accessToken;

        // 2. 保存新 Token
        localStorage.setItem('accessToken', newAccessToken);
        
        // 3. 设置当前失败请求的 Header
        config.headers['Authorization'] = `Bearer ${newAccessToken}`;

        // 4. 执行队列中的请求
        requestsQueue.forEach((cb) => cb(newAccessToken));
        requestsQueue = []; // 清空队列

        // 5. 重发当前请求
        return instance(config);

      } catch (refreshError) {
        // 刷新失败(Refresh Token 也过期了,或者被篡改)
        // 执行登出逻辑
        handleLogout();
        return Promise.reject(refreshError);
      } finally {
        // 释放锁
        isRefreshing = false;
      }
    }

    return Promise.reject(error);
  }
);

function handleLogout() {
  localStorage.clear();
  window.location.href = '/login';
}

3. 策略二:主动处理(请求前检查)

在发送请求之前,先解码 Token(JWT),判断 exp(过期时间)是否已到。如果快过期了,先刷新再发请求。

优点:减少了一次必然失败的 HTTP 请求,响应更快。
缺点:依赖客户端时间(虽然可以留 buffer),且需要引入解码库。

代码实现:

需要安装 jwt-decode
npm install jwt-decode

javascript
import jwt_decode from "jwt-decode";
import axios from "axios";

// 检查 Token 是否过期
const isTokenExpired = (token) => {
  try {
    const decoded = jwt_decode(token);
    const currentTime = Date.now() / 1000;
    // 预留 10 秒缓冲时间,避免请求在网络传输中过期
    return decoded.exp < currentTime + 10;
  } catch (e) {
    return true;
  }
};

// 请求拦截器
instance.interceptors.request.use(async (config) => {
  let token = localStorage.getItem('accessToken');

  if (token && isTokenExpired(token)) {
    // Token 过期,尝试刷新
    try {
       const { data } = await axios.post('/auth/refresh-token', {
          refreshToken: localStorage.getItem('refreshToken')
       });
       token = data.accessToken;
       localStorage.setItem('accessToken', token);
    } catch (e) {
       handleLogout();
       return Promise.reject(e);
    }
  }

  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`;
  }
  
  return config;
}, (error) => {
  return Promise.reject(error);
});

4. 策略三:定时器自动刷新(静默刷新)

在用户登录成功或页面加载时,解析 Token 的过期时间,设置一个 setTimeout,在过期前自动请求刷新接口。

  • 适用场景:需要极高实时性的应用(如股票交易、即时通讯),不希望用户操作时有任何因为刷新 Token 带来的延迟。
  • 缺点:如果用户打开页面后挂机,会一直消耗服务器资源刷新 Token(可以通过监听鼠标/键盘事件来优化,长时间无操作不刷新)。

5. 总结与最佳实践

在实际的大型项目中,通常采用 组合拳

  1. 首选被动处理(拦截器):这是兜底方案,必须要有。因为它能处理所有意外情况(如管理员手动封禁了 Token,虽然没过期但失效了)。
  2. 可选主动处理:为了极致体验,可以在路由跳转前或关键操作前进行主动检查。
  3. 处理并发:务必在拦截器中实现 isRefreshing 锁和 requestsQueue 队列,否则后端日志会报错,前端也会卡顿。
  4. 彻底失败后的处理:如果 Refresh Token 也过期了(或者接口报错),必须执行 logout 清理本地存储并重定向到登录页。

安全提示

  • 尽量不要把 Token 放在 localStorage,容易被 XSS 攻击窃取。
  • 更安全的做法是:Access Token 放在内存(变量)中,Refresh Token 放在 HttpOnly Cookie 中。这样前端 JS 读不到 Refresh Token,攻击者无法伪造刷新请求。
00:00
00:00