使用Axios进行无感刷新Token
使用Axios结合后端完成无感刷新token
·
前言
本人在开发项目时,在做登录模块时,参考了oauth2,在用户认证成功后会返回给前端一些令牌相关数据。接下来,再用进行接口请求时,前端根据令牌数据进行一系列的判断,然后做出最好的选择。
举个例子:
- 假如后端生成的令牌的有效期是10min,刷新令牌的有效期是1h。
- 当用户在10min内没有操作页面,但用户在最后一次操作后的1h内进行操作页面,那么前端就会使用刷新令牌请求后端获取新的令牌,然后携带新的令牌继续请求(底层实现,用户无感知)
- 当用户操作时,距上一次操作1h后,那么刷新令牌也是无效的,前端会将用户跳转到登录页面进行登录操作。
实现
令牌格式
本人定义的格式如下,其中第一层数据是我封装全局响应,而data内的数据就是我们今天需要说明的令牌数据:
{
"status":200,
"code":"0",
"clientMessage":"执行成功",
"data":{
"accessToken":"3aa501cd651945a9b4829ae166fca518",
"refreshToken":"6d1ceaf3f35d429491b273e7dd7e5daf",
"accessExpires":"2022-09-24 16:19:53",
"refreshExpires":"2022-09-26 15:19:53"
},
"dataMap":{},
"timestamp":"2022-09-24 15:19:53"
}
这4个字段都有各自的用途:
- accessToken:访问令牌,请求接口时需要携带。
- refreshToken:刷新令牌,在访问令牌失效时使用。
- accessExpires:访问令牌的有效截止时间。
- refreshExpires:刷新令牌的有效截止时间。
后端准备
在整个无感刷新令牌中后端需要准备的东西并不多,只需要准备如下内容即可:
- 登录接口,返回认证信息(2个令牌和令牌各自的有效截止时间),格式参考上面的令牌格式
- 刷新令牌接口:根据刷新令牌获取新的令牌(2个令牌和令牌各自的有效截止时间),格式参考上面的令牌格式
前端准备
我这里使用的是Axios,在axios的请求拦截器中进行处理逻辑。
- 首先定义全局变量:
// 按照axios官方提示需要引入这两步(取消axios的请求)
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
// 全局变量,记录是否正在执行刷新令牌任务
let isRefreshing = false
// 需要取消请求的集合
const cancelTokens = []
- 定义Axios请求拦截器:
service.interceptors.request.use(async config => {
/*
判断当前时间 accessExpires refreshExpires 三个时间
1. 当前时间小于accessExpires 正常请求
2. 当前时间大于accessExpires,但小于refreshExpires 先刷新令牌在正常请求
3. 当前时间大于refreshExpires正常请求(响应拦截器会将其重定向登录页)
*/
// 自定义的方法,用来获取toke对象,token的格式就是上面令牌的格式(有4个属性)
const token = LocalStorageUtil.getToken();
// 判断当前请求接口既不是登录请求,也不是刷新令牌请求时
if (validateUrlNotAuthentication(config.url)) {
const now = new Date()
// 判断是否需要请求刷新令牌接口(当前时间大于accessExpires,但小于refreshExpires 先刷新令牌在正常请求)
const flag = token && validateDate(token.accessExpires) && validateDate(token.refreshExpires) && now > new Date(token.accessExpires) && now < new Date(token.refreshExpires)
// 需要请求刷新令牌接口
if (flag) {
// 将本次请求放入待取消请求集合
config.cancelToken = source.token;
cancelTokens.push(() => source.cancel("令牌无效取消请求"));
// 调用刷新令牌方法,开始刷新本地令牌
await refreshingToken(token, config)
}
}
// 在请求头中添加token(这里再次使用方法获取实时的令牌,因为刷新令牌会修改本地存储的token)
if (LocalStorageUtil.getToken() && LocalStorageUtil.getAccessToken()) {
// eslint-disable-next-line require-atomic-updates
config.headers[AUTHORIZATION] = BEARER + LocalStorageUtil.getAccessToken()
}
console.log("请求拦截器", config)
return config
}, error => {
// do something with request error
console.log(error) // for debug
return Promise.reject(error)
})
- 定义Axios响应拦截器:
service.interceptors.response.use(response => {
console.log('响应拦截:', response)
// 响应码
const { status, config } = response
const result = response.data
// 响应码401,需要重新登录(或使用无感刷新token)
if (status === 401) {
// 清空用户登录状态
store.dispatch('user/resetToken')
// 跳转到登录页
Router.push({ path: '/login'})
return Promise.reject(result)
}
if (status < 200 || status >= 400) {
// 设置响应为错误,
return Promise.reject(result)
}
// 当是blob类型的响应时,返回完整的response,方便获取响应头等信息
if (config.responseType && config.responseType === 'blob') {
return Promise.resolve(response)
}
// 返回data内的数据(去掉axios和后端的封装外层数据,只保留data)
return Promise.resolve(result.data)
}, error => {
/*
后台返回5xx进入该函数内。
*/
const data = error.response.data;
console.log('err', error, data) // error 就是后端接口封装的对象
// 没有 doNotHandleErrorMessage 属性时,或 doNotHandleErrorMessage=false时 弹出提示信息
if (data && data.dataMap && !data.dataMap[DO_NOT_HANDLE_ERROR_MESSAGE]) {
Message({
message: data.clientMessage,
type: 'error',
duration: 5 * 1000
})
}
return Promise.reject(error)
})
- 定义刷新令牌方法如下:
/**
* 刷新令牌
* @param token 令牌对象
* @param config axios的配置对象
*/
async function refreshingToken(token, config) {
if (!isRefreshing) {
isRefreshing = true
return new Promise((resolve, reject) => {
// 请求刷新令牌
refresh(token.refreshToken).then(data => {
// 生成token对象
const newToken = new Token(data.accessToken, data.refreshToken, data.accessExpires, data.refreshExpires)
// 设置token对象
LocalStorageUtil.set(TOKEN_LOCAL_STORAGE, newToken)
resolve(data)
}).catch(data => {
// 刷新令牌失败,直接跳转登录界面
store.dispatch('user/resetToken')
if (!data.dataMap[DO_NOT_HANDLE_ERROR_MESSAGE]) {
Message({
message: '登录已过期,请重新登录',
type: 'error',
duration: 5 * 1000
})
data.dataMap[DO_NOT_HANDLE_ERROR_MESSAGE] = true
}
Router.push({ path: '/login'})
// 取消队列中的请求
cancelTokens.forEach(cancel => {
cancel().catch()
})
reject(data)
}).finally(() => {
// 恢复变量值
isRefreshing = false
})
})
} else {
// 阻塞当前请求
return new Promise((resolve, reject) => {
const id = setInterval(() => {
if (!isRefreshing) {
clearTimeout(id)
resolve();
}
}, 1000);
});
}
}
总结
在这之前,我最开始选择的是在响应拦截器进行无感刷新令牌的逻辑处理,并且也已经完成了工作,但是我发现在响应拦截器处理会有一个问题,就是用户会丢失一次请求的操作(这里是指跟页面数据渲染)
具体举个例子:
比如当前用户已经很长时间没操作了(10min),这个时候访问令牌已经失效了,当他再次请求时,会使用刷新令牌获取新的请求。如果这个逻辑是写在Axios的响应拦截器里,那么即使在补发请求,也不会恢复用户和页面的交互。比如查询表格数据,虽然请求补发了,但是数据不会自动渲染。
这也是本次花费时间将其进行优化的主要原因!
更多推荐
已为社区贡献5条内容
所有评论(0)