这个实现是基于后端支持双token的单点登录模式而论述的。
如此,双token模式下,token 分为长时token和短时token。当某个接口触发短时token过期,我们需要借助长时token获取到新的短时token,之后再重新调取之前触发token过期而中断的接口。这个过程用户无感知。
以axios为例,可以实现一个逻辑函数:
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全局封装中拦截短token过期状态并调用tokenHoldHandle,拦截长token过期状态跳出到登录页重新登陆:
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
}
// 创建axios实例
const instance= axios.create({
withCredentials: true,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json;charset=UTF-8',
},
})
// 请求拦截
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)
})
// 响应拦截
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!)) {
if(error.response.code==='shortTokenExpired'){
// 静默更新短token
return tokenHoldHandle(requestConfig)
}else{
// 中断正在调取的其他接口,并退出到登录页
abortControllers.forEach((item) => {
item.controller.abort()
});
abortControllers = []
location.href='/login';
}
} else {
console.error('接口返回异常,请重试!')
}
return Promise.reject(error)
})
export default instance;
以上为伪代码,以举例说明为目的,未经测试。