当前位置: 首页 > news >正文

封装Axios拦截器实现用户无感刷新AccessToken实践指南

一、背景与需求场景

1.1 单点登录体系中的Token管理

在单点登录(SSO)体系下,用户登录后系统会颁发两种令牌:

  • AccessToken:短期有效(2小时),用于接口鉴权

  • RefreshToken:长期有效(7天),用于刷新令牌

1.2 核心需求痛点

当AccessToken过期时,前端需要实现:

  1. 无感知刷新:用户操作不中断

  2. 并发控制:避免多个请求重复刷新

  3. 异常处理:RefreshToken失效时跳转登录


二、技术实现方案

2.1 整体实现思路

采用响应拦截器方案,当接口返回401时触发刷新机制:

2.2 核心代码实现

2.2.1 初始化Axios实例
const instance = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 10000
})
2.2.2 响应拦截器实现
// 刷新状态标记,用于记录当前是否正在进行刷新 token 的操作
// 避免在刷新 token 过程中重复发起刷新请求
let isRefreshing = false;
// 请求等待队列,用于存储在刷新 token 期间被拦截的请求
// 当新的 access_token 获取成功后,会依次处理这些请求
let requests = [];

// 为 axios 实例添加响应拦截器
// 响应拦截器会在每次请求响应回来时触发,可用于对响应进行统一处理
instance.interceptors.response.use(
  // 当响应成功时,直接返回响应对象,不做额外处理
  response => response,
  // 当响应出现错误时的处理逻辑
  error => {
    // 从错误对象中解构出响应对象和请求配置对象
    const { response, config } = error;

    // 判断响应状态码是否为 401(未授权),并且请求的 URL 不包含 '/auth/refresh'
    // 排除刷新 token 请求本身出现 401 错误的情况,避免无限循环
    if (response.status === 401 && !config.url.includes('/auth/refresh')) {
      // 如果当前没有正在进行刷新 token 的操作
      if (!isRefreshing) {
        // 标记正在进行刷新 token 的操作
        isRefreshing = true;
        // 调用刷新 token 的函数
        return refreshToken()
          .then(({ access_token }) => {
            // 刷新 token 成功,将新的 access_token 存储起来
            setToken(access_token);
            // 更新当前请求的请求头,使用新的 access_token
            config.headers.Authorization = `Bearer ${access_token}`;
            // 遍历请求等待队列,依次执行队列中的回调函数,并传入新的 access_token
            requests.forEach(cb => cb(access_token));
            // 清空请求等待队列
            requests = [];
            // 使用更新后的请求配置重新发起请求,并返回请求结果
            return instance(config);
          })
          .catch(() => {
            // 刷新 token 失败,调用登出函数,可能会清除本地存储的 token 并重定向到登录页
            logout();
          })
          .finally(() => {
            // 无论刷新 token 成功还是失败,都将刷新状态标记设置为 false,表示刷新操作结束
            isRefreshing = false;
          });
      }
      // 如果当前正在进行刷新 token 的操作
      return new Promise(resolve => {
        // 将当前请求封装成一个回调函数,加入到请求等待队列中
        requests.push(newToken => {
          // 当新的 access_token 获取成功后,更新当前请求的请求头
          config.headers.Authorization = `Bearer ${newToken}`;
          // 使用更新后的请求配置重新发起请求,并将结果传递给 resolve 函数
          resolve(instance(config));
        });
      });
    }
    // 如果不是 401 错误或者是刷新 token 请求本身出现错误,直接将错误对象抛出
    return Promise.reject(error);
  }
);

2.3 关键代码解析

代码部分功能说明
isRefreshing防止重复刷新的状态锁
requests存储等待中的请求队列
config.url.includes排除刷新接口本身
requests.forEach令牌刷新后重发所有挂起请求
new Promise创建待处理的Promise对象

三、方案优化点

3.1 防止并发刷新

通过isRefreshing状态锁实现:

if (!isRefreshing) {
  isRefreshing = true
  // 刷新逻辑...
}

3.2 请求队列处理

使用闭包存储请求配置:

requests.push(newToken => {
  config.headers.Authorization = `Bearer ${newToken}`
  resolve(instance(config))
})

3.3 安全增强措施

  1. 加密存储Token到Cookie

  2. 设置HttpOnly属性

  3. 启用Secure传输


四、方案对比分析

方案优点缺点
请求拦截器方案节省无效请求依赖本地时间准确性
响应拦截器方案时间判断更可靠多一次失败请求
服务端代理方案前端完全无感知需要服务端支持
WebSocket方案实时性最好实现复杂,资源消耗大

五、完整实现代码

// utils/request.js
import axios from 'axios'

let isRefreshing = false
let requests = []

const instance = axios.create({
  baseURL: process.env.VUE_APP_BASE_API,
  timeout: 10000
})

// 响应拦截器
instance.interceptors.response.use(
  response => response,
  error => {
    if (!error.response) return Promise.reject(error)
    
    const { status, config } = error.response
    if (status === 401 && !config._retry) {
      config._retry = true
      
      if (!isRefreshing) {
        isRefreshing = true
        return refreshToken()
          .then(({ access_token }) => {
            setToken(access_token)
            instance.defaults.headers.common['Authorization'] = `Bearer ${access_token}`
            requests.forEach(cb => cb(access_token))
            requests = []
            return instance(config)
          })
          .catch(err => {
            clearToken()
            redirectLogin()
            return Promise.reject(err)
          })
          .finally(() => isRefreshing = false)
      }
      
      return new Promise(resolve => {
        requests.push(token => {
          config.headers['Authorization'] = `Bearer ${token}`
          resolve(instance(config))
        })
      })
    }
    return Promise.reject(error)
  }
)

// 刷新Token方法
async function refreshToken() {
  try {
    const { refresh_token } = getToken()
    return await axios.post('/auth/refresh', { refresh_token })
  } catch (err) {
    throw new Error('刷新令牌失败')
  }
}

六、总结与展望

本文实现的方案具有以下优势:

  1. 用户无感知:自动处理令牌刷新

  2. 高可靠性:完善的错误处理机制

  3. 高性能:请求队列避免重复刷新

后续优化方向:

  1. 添加请求重试次数限制

  2. 实现滑动过期时间

  3. 增加网络异常处理

  4. 结合WebSocket实时更新

通过合理的前端Token管理策略,可以显著提升用户体验,降低重复登录频率。本方案已在生产环境稳定运行,可满足大多数SSO场景需求。

相关文章:

  • 简单创建一个Django项目并配置neo4j数据库
  • Scratch 3.0安装包,支持Win7/10/11、Mac电脑手机平板、少儿便编程的启蒙软件。
  • SQL99 多表查询
  • 成功破解加密机制,研究人员解锁LinuxESXi Akira勒索软件
  • 单片机技术
  • C++复试笔记(三)
  • flutter实践:断点调试踩坑
  • 循环遍历 Java 集合中元素的方法总结
  • 前端开发:混合技术栈的应用
  • 基于异构特征融合与轻量级集成学习的软件漏洞挖掘方案设计与Python实现
  • Spring Boot + InfluxDB 实现高效数据存储与查询
  • 总结 HTTPS 的加密流程
  • markdown 转 word 工具 ‌Pandoc‌
  • 微信小程序wx.request接口报错(errno: 600001, errMsg: “request:fail -2:net::ERR_FAILED“)
  • 从以太网 II 到 VLAN 和 Jumbo Frame:数据帧格式解读
  • 【xv6操作系统】系统调用与traps机制解析及实验设计
  • 《TCP/IP网络编程》学习笔记 | Chapter 18:多线程服务器端的实现
  • JSON数据对比 vue3 (可直接粘贴食用)
  • vue 自行封装组件,类似于el-tree组件结构
  • 珠算之加减法中出现负数情况
  • 证监会发布《上市公司募集资金监管规则》,6月15日起施行
  • 【社论】打破“隐形高墙”,让老年人更好融入社会
  • 押井守在30年前创造的虚拟世界何以比当下更超前?
  • 外交部:中方对美芬太尼反制仍然有效
  • 美国和沙特签署上千亿美元军售协议
  • 京东一季度净利增长五成,营收增速创近三年新高,称外卖业务取得显著进展