某个接口触发token过期,刷新token后重新调取接口,要求期间用户无感知
let tokenRefreshing = false // 是否在调取刷新token的接口
let retryQueue = [] // 重试队列,以备获取token后重新发起请求
let abortControllers = [] // 撤销队列,以备将请求撤销
function tokenHoldHandle(requestConfig) {
if (tokenRefreshing) {
// 正在刷新 token,将请求加入队列
return new Promise((resolve) => {
retryQueue.push(requestConfig)
})
} else {
tokenRefreshing = true;
return refreshToken().then((res) => { // 刷新 session
setToken(res.sessionId)
retryQueue.forEach((config) => instance(config)); // 重新发起队列中的请求
retryQueue = []; // 清空队列
return instance(requestConfig) // 重新调触发refresh的接口
}).catch((error) => {
const errorList = error.response?.data?.errorList || []
const firstError = errorList[0] || {}
passiveOut(firstError.errorMessage || 'Token 获取异常,请重新登录!')
}).finally(() => {
tokenRefreshing = false
})
}
}
以axios为例
import axios from 'axios'
import { refreshToken } from '@/apis/login'
/**
* 不依赖token的接口判断,比如调取token的接口、登录接口等
*/
function withoutTokenJudge(requestURL){
const withoutTokenAPIs = [
'/auth',
'/login',
'/sendVerifyCode',
'/refreshToken',
]
return withoutTokenAPIs.some(url => {
return new RegExp(`^${url}`).test(requestURL || '')
})
}
function setConfig(config){
const token = Cookies.get(TOKEN_NAME)
const controller = new AbortController();
// 不需要 auth token 的接口
if (withoutTokenJudge(config)) {
config.timeout = 30 * 1000
config.headers["sub-sys-kbn"] = "aga"; // 其他头信息
......
} else {
config.headers.Authorization = `Bearer ${token}`; // 约定的 token 传参
config.timeout = 60 * 1000
config.signal = controller.signal
config._controller = controller; // 可被控制的接口附上控制实例
}
return config
}
const instance= axios.create({
withCredentials: true,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json;charset=UTF-8',
},
})
// axios实例请求发起拦截
instance.interceptors.request.use(config => {
const updatedConfig = setConfig(config)
if (updatedConfig._controller) {
// 加入队列,以备撤销请求
abortControllers.push({
url: updatedConfig.url,
method: updatedConfig.method,
controller: updatedConfig._controller as AbortController
})
}
return updatedConfig
}, (error) => {
console.log('instance.interceptors.request', error)
return Promise.reject(error)
})
// axios实例响应拦截
instance.interceptors.response.use(async (response) => {
// TODO 错误码匹配反馈
if (response.status === 200) {
return response.data
} else {
console.error('接口返回异常,请重试!')
return response.data
}
}, async (error) => {
const requestConfig = error.config;
// 错误处理
if (error.response.status === 401) && !withoutTokenJudge(requestConfig.url!)) {
// 静默更新token逻辑
return tokenHoldHandle(requestConfig)
} else {
abortControllers.forEach((item) => {
item.controller.abort() // 取消所有请求
});
abortControllers = []
console.error('接口返回异常,请重试!')
}
return Promise.reject(error)
})
export default instance;