oauth2里有两种token:一种是访问token(access_token,时效一般比较短),另一种是刷新token(refresh_token,时效性一般长一些)。接口在请求一些受限资源时需要携带access_token;当access_token过期时通过refresh来更新,一切都很合理,然而依然存在携带过期access_token接口请求的风险。因此,处理好token的维护与续期就显得特别重要了。本文就探讨一下这个话题
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
如何知道access_token过期了?
一种最简单的方式是添加响应拦截,在拦截器里判断响应的状态码,一般为401未授权表示token无效,这时我们就可以发起refresh操作,刷新完毕再重试该接口即可:
interceptors.response.use(
function (response) {},
function (error) {
//401为异常响应,走这里
if(statusCode === 401){
return request('/refresh_token',{refresh_token:'xxxxxx'}).then(res=>{
//token刷新成功,重试之前失败的接口
if(res.data.success === true){
//直接从error对象里取出失败接口的请求配置信息(error.config)
return request(error.config)
}else{
//token刷新失败,做一些特殊处理吧
}
}).catch(error=>{
//token刷新出错,做一些特殊处理吧
})
}
console.log('http-error', error)
return Promise.reject(error)
},
)
okay,我们处理了401的情况并增加了接口重试,需要注意避免进入死循环: 一个401的接口导致了refresh行为,refresh行为也报了401的错误,再次触发refresh行为,周而复始...。所以需要跟server端协商好接口的表征,或者将refresh请求排除401拦截之外
看起来好像没什么问题,不过我们忽略了接口的并发性:在触发refresh后和refresh成功前,可能会发起其他的接口请求,这些接口依然会报401,他们会重复refresh的动作
我们稍微修改一下代码:
//刷新状态
let isRefreshing = false;
//记录刷新进行时并发接口resolve函数的队列
const queueWhenRefresh = [];
interceptors.response.use(
function (response) {},
function (error) {
//401为异常响应,走这里
if(statusCode === 401){
if(!isRefreshing){
isRefreshing = true;
return request('/refresh_token',{refresh_token:'xxxxxx'}).then(res=>{
//修改状态
isRefreshing = false;
//token刷新成功,重试之前失败的接口
if(res.data.success === true){
//处理等待的接口
queueWhenRefresh.forEach(item=>item.resolve())
//直接从error对象里取出失败接口的请求配置信息(error.config)
return request(error.config)
}else{
//token刷新失败,做一些特殊处理吧
}
}).catch(error=>{
//修改状态
isRefreshing = false;
//token刷新出错,做一些特殊处理吧
})
}else{
return new Promise((res)=>{
queueWhenRefresh.push(res);
}).then(()=>{
//重试接口
return request(error.config)
})
}
}
console.log('http-error', error)
return Promise.reject(error)
},
)
这样,我们就不会同时发起多个refresh请求了。
看起来好像挺好的,但是感觉怪怪的,通过接口响应码来判定token失效总体比较被动,我们能不能在接口发起时就先获取token的有效性,有效时再发起请求?
答案是可以的。
提前检测访问凭证的有效性
服务端在返回token时会告诉我们这个token在多久后会过期,我们会将它存储起来。使用之前比对一下时效性。
我们其实可以在请求拦截里添加异步拦截:
request.interceptors.request.use(function (config) {
const tokenStatus = await getTokenStatus()
//token有效
if (tokenStatus === true) {
return config
} else {
return Promise.reject('err-----')
}
},function(err){})
function getTokenStatus(){
return new Promise((res,rej)=>{
const isValid = Number(localStorage.getItem('access_expiretimestamp')) > Date.now();
if(localStorage.getItem('access_token') && isValid){
res(true);
}else{
res(request('/refresh_token',{refresh_token:'xxxxx'}).then(res=>{
if(res.data.success){
return true;
}
}))
}
})
}
好多了不是吗?什么,主动性还是不够?我们应该保证用户活跃期间token始终保持良好的有效期:
- 监听UI事件,启动刷新。
- 启动一个定时任务刷新token
想法很好,然而上述动作在并发的情况下会产生新的问题:一次刷新和一个正常发起的请求同时进行。由于服务端接到了刷新请求将token作废,导致后面的接口返回了401,虽然这个接口请求时的token是合法有效的。
怎么破解这个问题,其实把第一个方案里的队列思想搬过来就可以了,当执行刷新时,接口请求一律缓存,待刷新结束再执行请求。具体细节不再赘述。
最后
感谢阅读,如有任何问题欢迎留言讨论!