在 Vue 项目中统一处理 Token,最稳妥的做法是在 Axios 实例创建后,通过请求拦截器自动注入 Token,并在响应拦截器中处理过期刷新或跳转登录,适用于绝大多数前后端分离场景。
先说结论:封装拦截器是标准做法,但要注意 Token 存储安全和刷新逻辑的死循环问题
- 适合:前后端分离、使用 Bearer Token 鉴权的项目
- 先看:Token 存在 localStorage 还是 Cookie,决定读取方式
- 建议:响应拦截器中处理 401 状态码时,增加防止无限刷新请求的标志
- 安全:高敏感场景建议使用 HttpOnly Cookie 替代 localStorage 存储 Token
完整封装代码示例
以下代码包含请求拦截、响应拦截、Token 无感刷新及并发锁机制,可直接参考修改 src/utils/request.js:
import axios from 'axios'
import { ElMessage } from 'element-plus' // 根据 UI 库调整
import router from '@/router'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '/api',
timeout: 5000
})
let isRefreshing = false // 刷新锁
let requestQueue = [] // 请求队列
// 执行队列中的请求
const executeQueue = (token) => {
requestQueue.forEach(cb => cb(token))
requestQueue = []
}
// 请求拦截器
service.interceptors.request.use(config => {
// 优先从 Cookie 读取,其次 localStorage,根据实际存储方式调整
const token = document.cookie.replace(/(?:(?:^|.*;)\s*token\s*=\s*([^;]*).*)/, "$1") || localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = `Bearer ${token}`
}
return config
}, error => {
return Promise.reject(error)
})
// 响应拦截器
service.interceptors.response.use(response => {
const res = response.data
// 根据后端约定判断业务错误,例如 code !== 200
if (res.code !== 200) {
ElMessage.error(res.message || 'Error')
return Promise.reject(new Error(res.message || 'Error'))
}
return res
}, error => {
if (error.response && error.response.status === 401) {
if (!isRefreshing) {
isRefreshing = true
// 调用刷新 Token 接口
return refreshToken().then(newToken => {
// 刷新成功,更新存储
localStorage.setItem('token', newToken)
executeQueue(newToken)
// 重试当前请求
error.config.headers['Authorization'] = `Bearer ${newToken}`
return service(error.config)
}).catch(() => {
// 刷新失败,跳转登录
localStorage.removeItem('token')
router.push('/login')
return Promise.reject(error)
}).finally(() => {
isRefreshing = false
})
} else {
// 正在刷新,将当前请求加入队列
return new Promise((resolve) => {
requestQueue.push((token) => {
error.config.headers['Authorization'] = `Bearer ${token}`
resolve(service(error.config))
})
})
}
}
return Promise.reject(error)
})
// 模拟刷新 Token 函数,实际需替换为真实 API
function refreshToken() {
return new Promise((resolve, reject) => {
axios.post('/auth/refresh').then(res => {
resolve(res.data.token)
}).catch(() => {
reject()
})
})
}
export default service核心逻辑解析
1. 请求拦截:统一读取存储中的 Token 并写入 Header,避免每个请求重复编写。
2. 响应拦截:捕获 401 状态码,区分是否正在刷新 Token。
3. 并发锁机制:使用 isRefreshingrequestQueue 暂存并发请求,待刷新完成后统一重试。
4. 异常处理:刷新失败或业务错误时,统一清理本地存储并跳转登录页,防止死循环。
怎么验证是否生效
1. 检查请求头:打开浏览器开发者工具 Network 面板,发起业务请求,确认 Request Headers 中是否自动携带 Authorization 字段。
2. 模拟过期:手动将 localStorage 中的 Token 修改为过期值,发起请求,观察是否自动触发刷新接口且业务请求最终成功(状态码 200)。
3. 并发测试:同时发起多个请求,确保刷新 Token 接口只被调用一次,其他请求在刷新后自动重试。
常见风险与排查
1. 存储安全:localStorage 易受 XSS 攻击,若项目安全性要求高,建议后端配置 HttpOnly Cookie 存储 Token,前端无需手动读取,axios 会自动携带。
2. 刷新死循环:确保刷新 Token 的接口本身不会触发 401 拦截逻辑,可在刷新请求配置中增加 skipAuth: true 并在拦截器中判断跳过。
3. 并发问题:多个请求同时 401 时,必须加锁控制,否则会导致多次刷新请求,消耗服务器资源且可能导致 Token 失效。
4. 单元测试:在单元测试环境中,记得重置拦截器或 Mock axios 实例,避免污染其他测试用例。
参考来源
1. Axios 官方文档 - Interceptors 章节,https://github.com/axios/axios#interceptors