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

Axios响应拦截器实战:如何优雅处理401错误与Token自动续期

1. 为什么需要处理401错误与Token自动续期

在前后端分离的现代Web应用中,401未授权错误就像酒店过期的房卡——明明昨天还能用,今天突然失效了。想象一下这样的场景:用户正在填写复杂的表单,点击提交时却突然跳转到登录页,所有输入内容瞬间消失。这种体验就像坐过山车时突然停电,既尴尬又令人沮丧。

Token机制的本质是会话管理的安全措施。典型的JWT Token包含三部分:头部(Header)、载荷(Payload)和签名(Signature)。其中Payload中会记录过期时间(exp字段),就像食品包装上的保质期标签。当服务器检测到Token过期,就会返回401状态码,相当于告诉前端:"你的通行证失效了,请重新认证"。

传统处理方式简单粗暴:直接跳转登录页。但现代Web应用追求的是无感知体验,就像手机自动切换WiFi和4G网络,用户根本察觉不到变化。要实现这种丝滑过渡,我们需要两个关键武器:

  1. 双Token机制:短期有效的Access Token(通常2小时)和长期有效的Refresh Token(通常7-14天)
  2. Axios响应拦截器:作为前端请求的"安检通道",能捕获所有响应并进行统一处理

2. 双Token机制的工作原理

双Token机制就像银行账户的双重验证:Access Token是短信验证码,时效短但使用频繁;Refresh Token像是U盾,虽然使用次数少但有效期长。具体流程如下:

  1. 用户登录成功获得两个Token
  2. 前端使用Access Token访问API
  3. 当Access Token过期(返回401错误)
  4. 前端使用Refresh Token获取新Access Token
  5. 如果Refresh Token也过期,才要求用户重新登录

这种设计有三大优势:

  • 安全性:Access Token短期有效,即使泄露影响有限
  • 用户体验:减少频繁登录的打扰
  • 性能:比Session机制更节省服务器资源
// 典型登录响应数据结构 { "data": { "token": "eyJhbGciOi...", // Access Token "refresh_token": "eyJhbGciOi..." // Refresh Token } }

3. 基础版响应拦截器实现

让我们先实现一个基础版的拦截器,处理单个请求的Token刷新场景。这就像先学会骑自行车,再考虑怎么带人。

import axios from 'axios' import store from '@/store' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 5000 }) // 响应拦截器 service.interceptors.response.use( response => response, async error => { const { config, response } = error // 非401错误直接拒绝 if (response.status !== 401) { return Promise.reject(error) } // 检查是否存在refresh_token const refreshToken = store.state.user.refresh_token if (!refreshToken) { redirectToLogin() return Promise.reject(error) } try { // 尝试刷新Token const { data } = await axios.post('/auth/refresh', { refresh_token: refreshToken }) // 存储新Token store.commit('user/UPDATE_TOKEN', data.token) // 重发原始请求 config.headers.Authorization = `Bearer ${data.token}` return service(config) } catch (err) { // 刷新失败跳转登录 redirectToLogin() return Promise.reject(err) } } ) function redirectToLogin() { store.commit('user/LOGOUT') router.push(`/login?redirect=${encodeURIComponent(router.currentRoute.fullPath)}`) }

这个基础版本已经能处理大多数简单场景,但它有个致命缺陷——无法处理并发请求。就像十字路口没有红绿灯,多辆车同时通过就会发生事故。

4. 高级并发控制方案

当多个请求同时返回401时,如果每个请求都去刷新Token,就会造成:

  1. 重复刷新浪费资源
  2. 可能引发Race Condition(竞态条件)
  3. 后续请求可能携带已过期的Token

解决方案是引入三个关键概念:

  • 刷新标记(isRefreshing):标识是否正在刷新Token
  • 请求队列(requests):存储等待中的请求
  • Promise链:保证请求顺序执行
let isRefreshing = false let requests = [] service.interceptors.response.use( response => response, async error => { // ...省略基础判断逻辑... if (!isRefreshing) { isRefreshing = true try { const { data } = await axios.post('/auth/refresh', { refresh_token: store.state.user.refresh_token }) store.commit('user/UPDATE_TOKEN', data.token) // 执行等待队列 requests.forEach(cb => cb(data.token)) requests = [] return service(config) } catch (err) { redirectToLogin() return Promise.reject(err) } finally { isRefreshing = false } } else { // 将请求加入队列 return new Promise(resolve => { requests.push(token => { config.headers.Authorization = `Bearer ${token}` resolve(service(config)) }) }) } } )

这种设计就像机场的流量控制:

  1. 第一架飞机请求降落(触发刷新)
  2. 塔台标记跑道占用(isRefreshing = true)
  3. 后续飞机进入等待队列(requests.push)
  4. 跑道清空后按顺序放行(requests.forEach)

5. 实战中的边界情况处理

真实项目就像越野赛道,除了主干道还有各种需要应对的特殊情况:

5.1 白名单控制

不是所有接口都需要Token,比如登录、公开API等。我们需要配置白名单:

const WHITE_LIST = ['/auth/login', '/auth/refresh'] function isWhiteList(url) { return WHITE_LIST.some(path => url.includes(path)) } // 在请求拦截器中 service.interceptors.request.use(config => { if (!isWhiteList(config.url)) { config.headers.Authorization = `Bearer ${store.state.user.token}` } return config })

5.2 刷新Token失效

当Refresh Token也过期时,应该:

  1. 清除所有用户数据
  2. 跳转登录页
  3. 携带原始路由信息,方便登录后跳回
function redirectToLogin() { store.commit('user/RESET_STATE') const currentRoute = router.currentRoute router.push({ path: '/login', query: { redirect: currentRoute.fullPath } }) }

5.3 请求重试限制

为防止无限重试,应该:

  1. 设置最大重试次数
  2. 记录已重试次数
config.__retryCount = config.__retryCount || 0 if (config.__retryCount >= 3) { return Promise.reject(error) } config.__retryCount += 1

6. 性能优化与安全实践

完善的Token管理就像精心设计的安保系统,既要方便又要安全:

6.1 Token存储方案对比

存储方式安全性防XSS防CSRF备注
localStorage需自行实现过期逻辑
sessionStorage标签页关闭即失效
Cookie HttpOnly需配合SameSite属性
内存变量刷新页面会丢失

推荐做法:Access Token存内存,Refresh Token存HttpOnly Cookie

6.2 定时刷新策略

除了被动刷新,还可以主动刷新:

// 登录后设置定时器 let refreshTimer = null function scheduleRefresh(expiresIn) { clearTimeout(refreshTimer) // 提前5分钟刷新 const delay = (expiresIn - 300) * 1000 refreshTimer = setTimeout(async () => { try { await refreshToken() } catch (err) { console.error('定时刷新Token失败', err) } }, delay) }

6.3 安全增强措施

  1. Token绑定:将Token与设备指纹/IP绑定
  2. 使用限制:单个Refresh Token只能使用一次
  3. 监控报警:异常频繁刷新时触发安全警报

7. 完整代码实现与测试

最终版的拦截器应该像瑞士军刀一样功能全面:

import axios from 'axios' import store from '@/store' import router from '@/router' const WHITE_LIST = ['/auth/login', '/auth/refresh'] const MAX_RETRY = 3 const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) let isRefreshing = false let requests = [] function isWhiteList(url) { return WHITE_LIST.some(path => url.includes(path)) } function redirectToLogin() { store.commit('user/RESET_STATE') const currentRoute = router.currentRoute router.push({ path: '/login', query: { redirect: currentRoute.fullPath } }) } // 请求拦截器 service.interceptors.request.use(config => { if (!isWhiteList(config.url) && store.state.user.token) { config.headers.Authorization = `Bearer ${store.state.user.token}` } return config }) // 响应拦截器 service.interceptors.response.use( response => response, async error => { const { config, response } = error // 非401错误或白名单直接拒绝 if (!response || response.status !== 401 || isWhiteList(config.url)) { return Promise.reject(error) } // 超过重试次数 config.__retryCount = config.__retryCount || 0 if (config.__retryCount >= MAX_RETRY) { return Promise.reject(error) } config.__retryCount += 1 // 检查refresh_token if (!store.state.user.refresh_token) { redirectToLogin() return Promise.reject(error) } // 正在刷新,加入队列 if (isRefreshing) { return new Promise(resolve => { requests.push(token => { config.headers.Authorization = `Bearer ${token}` resolve(service(config)) }) }) } isRefreshing = true try { const { data } = await axios.post('/auth/refresh', { refresh_token: store.state.user.refresh_token }) store.commit('user/UPDATE_TOKEN', data.token) requests.forEach(cb => cb(data.token)) requests = [] return service(config) } catch (err) { redirectToLogin() return Promise.reject(err) } finally { isRefreshing = false } } ) export default service

测试时需要模拟以下场景:

  1. 单个请求Token过期
  2. 并发多个请求同时返回401
  3. Refresh Token过期的情况
  4. 网络不稳定的重试场景

可以使用Mock Service Worker等工具模拟各种异常情况:

import { setupWorker, rest } from 'msw' const worker = setupWorker( rest.get('/api/data', (req, res, ctx) => { const token = req.headers.get('Authorization') if (!token || !token.includes('valid')) { return res(ctx.status(401)) } return res(ctx.json({ data: 'test' })) }), rest.post('/auth/refresh', (req, res, ctx) => { const { refresh_token } = req.body if (refresh_token === 'expired') { return res(ctx.status(401)) } return res(ctx.json({ token: 'new_token' })) }) )

在实际项目中,这种无感刷新机制能让应用的健壮性提升一个等级。曾经有个电商项目上线后,客服反馈说用户经常抱怨填写长表单时被登出。接入这套方案后,相关投诉减少了90%以上。

http://www.jsqmd.com/news/564029/

相关文章:

  • 3分钟搞定跨平台:Whisky让你的Mac运行Windows应用零障碍
  • 多模态文档处理:Step3-VL-10B-Base与Typora的深度集成
  • 基于EFCore与领域事件驱动的敏感数据审计日志架构:实现不可篡改的变更追溯与合规性保障
  • 2026国内优质喷泉厂家推荐榜:呐喊喷泉/喷泉设备/四川音乐喷泉/室内喷泉/排湖喷泉/摇摆喷泉/水慕电影喷泉/水雾喷泉/选择指南 - 优质品牌商家
  • 本地硬盘装系统神器更新!WinToHDD v7.0,支持加密/多分区安装
  • 58:L应用数字取证AI:蓝队的证据收集
  • s2-proGPU利用率提升方案:批处理合成与异步请求性能压测报告
  • 保姆级教程:用Dify+博查WebSearch,5分钟给本地Ollama模型装上联网搜索大脑
  • 2026年比较好的污水处理聚合氯化铝/白色聚合氯化铝/山东工业级聚合氯化铝/山东聚合氯化铝优质供应商推荐 - 行业平台推荐
  • 2026年质量好的六轴数控机床/四轴数控机床品牌厂家推荐 - 行业平台推荐
  • Explain详解
  • CNN-BiGRU+BiGRU+CNN三模型多变量时间序列预测一键对比 Matlab代码
  • 突破限速:8大网盘直链解析方案全解析
  • 告别布局跳动!Android Dialog+EditText+软键盘的终极适配指南(含Kotlin代码)
  • 2026年格行随身WiFi代理项目分析:零成本物联网创业月入5万+实战指南 - 格行官方招商总部
  • 高考物理实验复习学习平台推荐(实测好用,告别低效刷题)
  • SkeyeRTMPClient拉取RTMP流扩展支持HEVC(H.265)解决方案
  • 2026年比较好的自激式文丘里湿式除尘器/矩激式湿式除尘器/抛丸湿式除尘器/抛光湿式除尘器厂家精选 - 行业平台推荐
  • 从Socket到RDMA:一个分布式数据库开发者的性能优化手记
  • 手把手教你用Arm Cortex-A715手册:从RAS到调试,一份给芯片设计者的实战笔记
  • vLLM-v0.17.1保姆级教程:vLLM + Weights Biases 实验跟踪实践
  • 鸿蒙元服务ArkTS开发方案
  • Ostrakon-VL-8B GPU算力优化:8B模型在A10/A100上vLLM吞吐提升300%实测
  • 用PyGame写个视频标注工具,我踩过的坑和优化思路(附完整代码)
  • undefined reference to `std::cout‘
  • 告别CPU瓶颈:NVJPEG硬件解码在Jetson边缘设备上的实战调优
  • 忍者像素绘卷镜像免配置:一键切换‘天界画坊’/‘木叶村’双主题UI
  • 单管烟囱塔选购:景区监控塔/火炬烟筒塔/烟囱塔架/烟囱塔止晃架/烟筒塔支架/监控铁塔/瞭望监控塔/碳钢烟囱塔/角钢监控塔/选择指南 - 优质品牌商家
  • Tao-8k助力网络安全:智能威胁情报分析与报告撰写
  • Arduino智能小车避坑指南:从TB6612驱动到HC-05蓝牙,新手最容易搞错的5个硬件连接点