如何实现无感刷新Token

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

流程如下

  • 用户登录成功后,接口返回 accessTokenrefrshToken。前端将这两个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重新执行一遍。

相关推荐
夜郎king25 分钟前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
夏幻灵1 小时前
HTML5里最常用的十大标签
前端·html·html5
Mr Xu_2 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js
未来龙皇小蓝2 小时前
RBAC前端架构-01:项目初始化
前端·架构
程序员agions2 小时前
2026年,微前端终于“死“了
前端·状态模式
万岳科技系统开发2 小时前
食堂采购系统源码库存扣减算法与并发控制实现详解
java·前端·数据库·算法
程序员猫哥_2 小时前
HTML 生成网页工具推荐:从手写代码到 AI 自动生成网页的进化路径
前端·人工智能·html
龙飞052 小时前
Systemd -systemctl - journalctl 速查表:服务管理 + 日志排障
linux·运维·前端·chrome·systemctl·journalctl
我爱加班、、2 小时前
Websocket能携带token过去后端吗
前端·后端·websocket
AAA阿giao2 小时前
从零拆解一个 React + TypeScript 的 TodoList:模块化、数据流与工程实践
前端·react.js·ui·typescript·前端框架