如何实现无感刷新Token

无感刷新 Token 需要前后端配合实现

流程如下

  • 用户登录成功后,接口返回 accessToken 和 refrshToken。前端将这两个token存在本地。
  • 后续业务接口请求时,需要在请求头上加上accessToken
  • 当accessToken过期时,调接口(带上refreshToken参数)重新获取 accessToken,然后将失败的、暂停的请求带上最新的accessToken重新处理一遍(队列管理这些请求)
  • 如果刷新 Token 失败,则跳转到登录页重新登录

伪代码如下

js 复制代码
import axios from 'axios';

// 创建一个新的axios实例
const api = axios.create({
  baseURL: '/api',
  timeout: 5000,
});

// ------------------- 请求拦截器 -------------------
api.interceptors.request.use(config => {
  const accessToken = localStorage.getItem('accessToken');
  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }
  return config;
}, error => {
  return Promise.reject(error);
});


// ------------------- 响应拦截器 -------------------

// 用于标记是否正在刷新token
let isRefreshing = false;
// 用于存储因为token过期而被挂起的请求
let requestsQueue = [];

api.interceptors.response.use(
  response => {
    return response;
  }, 
  async error => {
    const { config, response } = error;
    
    // 如果返回的HTTP状态码是401,说明access_token过期了
    if (response && response.status === 401) {
      
      // 如果当前没有在刷新token,那么我们就去刷新token
      if (!isRefreshing) {
        isRefreshing = true;

        try {
          // 调用刷新token的接口
          const { data } = await axios.post('/refresh-token', {
            refreshToken: localStorage.getItem('refreshToken') 
          });
          
          const newAccessToken = data.accessToken;
          localStorage.setItem('accessToken', newAccessToken);

          // token刷新成功后,重新执行所有被挂起的请求
          requestsQueue.forEach(cb => cb(newAccessToken));
          // 清空队列
          requestsQueue = [];

          // 把本次失败的请求也重新执行一次
          config.headers.Authorization = `Bearer ${newAccessToken}`;
          return api(config);

        } catch (refreshError) {
          // 如果刷新token也失败了,说明refreshToken也过期了
          // 此时只能清空本地存储,跳转到登录页
          console.error('Refresh token failed:', refreshError);
          localStorage.removeItem('accessToken');
          localStorage.removeItem('refreshToken');
          // window.location.href = '/login';
          return Promise.reject(refreshError);
        } finally {
          isRefreshing = false;
        }
      } else {
        // 如果当前正在刷新token,就把这次失败的请求,存储到队列里
        // 返回一个pending的Promise,等token刷新后再去执行
        return new Promise((resolve) => {
          requestsQueue.push((newAccessToken) => {
            config.headers.Authorization = `Bearer ${newAccessToken}`;
            resolve(api(config));
          });
        });
      }
    }
    
    return Promise.reject(error);
  }
);

export default api;

代码里面有两个地方需要注意:

  • isRefreshing 状态锁:

    这是为了解决并发问题。想象一下,如果一个页面同时发起了多个API请求,而accessToken刚好过期,多个请求会同时收到401。如果没有isRefreshing这个锁,它们会同时去调用/refresh-token接口,发起多次刷新请求,这是完全没有必要的浪费,甚至可能因为并发问题导致后端逻辑出错。

    有了这个锁,只有第一个收到401的请求,会真正去执行刷新逻辑。

  • requestsQueue 请求队列:

    比如现在有3个请求,第1个请求正在刷新Token(isRefreshing = true),后面那2个请求把它们的resolve函数推进一个队列(requestsQueue)里,暂时挂起。

    等第一个请求成功拿到新的accessToken后,再遍历这个队列,把所有被挂起的请求,用新的Token重新执行一遍。

相关推荐
子兮曰3 天前
jev-ultrafast 深度解析:7 秒订机票的浏览器 Agent 是如何炼成的
前端·后端·agent
子兮曰3 天前
Jev 爆发一周:7 秒 Agent 背后的 System One 生态与三场争议
前端·后端·ai编程
前端小万3 天前
写公众号赚了 3000 块后,我做了一款叫 "一键成稿" 的软件
前端·微信小程序
爱勇宝3 天前
ZCode 开源 24 小时:一份没有历史的账本,回答不了"有没有偷代码"
前端·后端·chatglm (智谱)
三十而立洋3 天前
Cookie 详解:从产生到安全,一次讲透
前端·javascript
卡布鲁3 天前
把一个 Vite + Vue3 应用塞进 qiankun (React + Umi3) 主站:十个坑的复盘
前端·javascript·react.js
汉堡大王95273 天前
Jev:不是聊天机器人, 而是一个智能 if 语句
前端·人工智能·后端
梦想很大很大3 天前
从运行事实到回归证据:Workrun 的 Telemetry 与 Evaluation 实践
前端·人工智能·后端
计算机魔术师3 天前
Meta Muse agent 接入 Shopify 的 Shop Pay 实现代理式购物
前端
沙蒿同学3 天前
我用 Go 搭了一条 AI Agent 流水线:从 1 张商品图到一整套淘宝详情页
前端·javascript·后端