一、需求描述:
前端登录后,后端返回acToken和acToken有效时间以及refreshToken,然后在request.headers带上acToken,当acToken过期时要用refreshToken去获取新的acToken,当refreshToken过期前端跳转到登录页,获取新的acToken时要做到用户无感知。
二、分析
当用户发起一个请求时,判断acToken是否已过期,若已过期则先调refreshToken
接口,拿到新的acToken后再继续执行之前的请求。
这个问题的难点在于:
1、当同时发起多个请求,而刷新acToken的接口还没返回,此时其他请求该如何处理?
2、假如请求中带有用户行为,比如一个购买按钮,点击它会先发起用户是否被禁用接口,如果没有禁用,则跳转到购买页,否则弹窗提示。
三、方案
1、在请求发起前拦截每个请求,判断acToken的有效时间是否已经过期,若已过期,则将请求挂起,先刷新acToken后再继续请求。
- 优点: 在请求前拦截,能节省请求,省流量。
- 缺点: 需要后端额外提供一个acToken过期时间的字段;使用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败。
PS:acToken有效时间建议是时间段,类似缓存的MaxAge,而不要是绝对时间。当服务器和本地时间不一致时,绝对时间会有问题。
2、不在请求前拦截,而是拦截返回后的数据。先发起请求,接口返回过期后,先刷新acToken,再进行一次重试。
- 优点:不需额外的acToken过期字段,不需判断时间。
- 缺点: 1、会消耗多一次请求,耗流量 2、请求then之后有用户行为,无法执行。
因为方案2没有解决到难点2,故此使用方案1,如有办法再研究
四、实现代码如下:
let isRefreshing = false
let requests =[]
function getJwtTokenVO(){
return localStorage.getItem('jwtTokenVO')?JSON.parse(localStorage.getItem('jwtTokenVO')):''
}
// request interceptor
service.interceptors.request.use(
config => {
// 过滤掉刷新token接口(避免死循环!!!!)和登录接口
if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
return config
}
const jwtTokenVO = getJwtTokenVO()
if (jwtTokenVO) {
config.headers={...config.headers,acToken:jwtTokenVO.accessToken}
const now = new Date().getTime()
const accessTokenExpire = jwtTokenVO.accessTokenExpire //这个是绝对时间戳
// 稳妥要提前一点时间刷新token
if (now > (accessTokenExpire-5000)) {
// 立即刷新token
if (!isRefreshing) {
console.log('正在刷新token')
isRefreshing = true
fetchRefreshToken({ token: jwtTokenVO.refreshToken }).then(resData => {
// 刷新token接口的时候可能引起refreshToken过期跳转登录页,这里没有进service.interceptors.response
if (resData.code * 1 === 5004) {
localStorage.removeItem('jwtTokenVO')
let href = window.location.href
/* refreshToken失效 这个code需要再修改 */
window.location.href = `${location.origin}${location.pathname}${location.search}#/login?orgId=${localStorage.getItem('lc_orgId')}&next=${href}`
return ''
}
if (resData.code * 1 === 1) {
console.log('重新获取accessToken 成功')
/* 重新赋值accessToken */
localStorage.setItem('jwtTokenVO', JSON.stringify(resData.data))
axios.defaults.headers['acToken'] = resData.data.accessToken
}
isRefreshing = false
return resData.data.accessToken
}).then((accessToken) => {
console.log('刷新token成功,执行请求队列')
if(accessToken){
requests.forEach(cb => cb(accessToken))
// 执行完成后,清空队列
requests = []
}
}).catch(res => {
console.error('刷新token error: ', res)
})
}
const requestList = new Promise((resolve) => {
requests.push((accessToken) => {
// 刷新config的旧token
config.headers['acToken'] = accessToken
resolve(config)
})
})
return requestList
//
}
}
return config
},
error => {
console.log(error) // for debug
return Promise.reject(error)
}
)
// 请求返回后拦截
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 5004) {
localStorage.removeItem('jwtTokenVO')
let href = window.location.href
/* refreshToken失效 这个code需要再修改 */
window.location.href = `${location.origin}${location.pathname}${location.search}#/login?orgId=${localStorage.getItem('lc_orgId')}&next=${href}`
}
return response
}, error => {
console.log('catch', error)
return Promise.reject(error)
})