方案一
思路:
后端需要返回一个token过期的时间,在请求发起前拦截每个请求,判断token的有效时间是否已经过期,如果已过期,则暂时将请求挂起,先刷新token后再继续请求。
前期思考:
- 返回过期时间是为了判断是否过期的,但是如果服务器时间和系统时间不一致呢?
- 如果同时发起多个请求时怎么办?
实现:
因为是在请求前进行拦截,所以这里用axios的axios.interceptors.request.use()
方法
首先,关于服务器时间和系统时间不一致的问题,我的解决方法是:
后端返回的过期时间应该是一个时间段,而不是绝对时间,这样就避免了二者时间不一致的问题。前端拿到token的有效时间后,在存到localStorage前,先依据系统时间转成绝对时间。
例如:当前时间是12点,有效时间是3600秒(1个小时),则存到localStorage的过期时间是13点的时间戳,使用时只需要判断这个绝对时间即可。
- 先不考虑复杂情况,正常接口请求
// 请求前先进行拦截
instance.interceptors.request.use((config) => {
//这里不单单是获取到token,还要一个token的过期时间,用来进行下面的判断
const tokenInfo = getToken()
/**
* 这里模拟一下tokenInfo的结构
* tokenInfo:{
* token:'fdasfeffafdsfasfdasfdas',
* tokenExpireTime:'36000'
* }
*/
//这里先拿到token,给每个请求添加一个token请求头
config.headers['token'] = tokenInfo.token
// 注意:登录接口和刷新token接口是不需要token的
if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
return config
}
//这里是拦截的关键代码
if (tokenInfo.token && tokenInfo.tokenExpireTime) {
const now = Date.now()
if (now >= tokenInfo.tokenExpireTime) { // 这里已经过期了。
//返回一个Promise,执行refreshToken后再return当前的config
return refreshToken().then(res => {
const { token, tokenExprieIn } = res.data
//重新把时间段转成绝对时间存起来
const tokenExpireTime = now + tokenExprieIn * 1000
instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
config.headers['token'] = token // 请求头重新赋值token
return config
}).catch(res => {
console.error('token刷新error', res)
})
}
}
return config
}, (error) => {
return Promise.reject(error)
})
- 多个请求同时发起
当同时发起多个请求,势必会多次调用refreshToken()
,这时只需要就需要一个isFlag
的状态来判断是否正在刷新token
let isFlag = false
instance.interceptors.request.use((config) => {
//这里不单单是获取到token,还要一个token的过期时间,用来进行下面的判断
const tokenInfo = getToken()
/**
* 这里模拟一下tokenInfo的结构
* tokenInfo:{
* token:'fdasfeffafdsfasfdasfdas',
* tokenExpireTime:'36000'
* }
*/
//这里先拿到token,给每个请求添加一个token请求头
config.headers['token'] = tokenInfo.token
// 注意:登录接口和刷新token接口是不需要token的
if (config.url.indexOf('/refreshToken') >= 0 || config.url.indexOf('/login') >= 0) {
return config
}
//这里是拦截的关键代码
if (tokenInfo.token && tokenInfo.tokenExpireTime) {
const now = Date.now()
if (now >= tokenInfo.tokenExpireTime) { // 这里已经过期了。
//返回一个Promise,执行refreshToken后再return当前的config
if (!isRefreshing) {
isFlag = true
return refreshToken().then(res => {
const { token, tokenExprieIn } = res.data
//重新把时间段转成绝对时间存起来
const tokenExpireTime = now + tokenExprieIn * 1000
instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
isFlag = false //一旦刷新成功,再重置回去
config.headers['token'] = token // 请求头重新赋值token
return config
}).catch(res => {
console.error('token刷新error', res)
})
}
}
}
return config
}, (error) => {
return Promise.reject(error)
})
通过isFlag
打断了多次请求后,需要用到一个Promise
来让所有请求挂起,这里我们把其他请求先存到一个数组里等待token刷新完来请求他。
这里我们再加一个.then
来一步一步进行
if (!isRefreshing) {
isFlag = true
return refreshToken().then(res => {
const { token, tokenExprieIn } = res.data
//重新把时间段转成绝对时间存起来
const tokenExpireTime = now + tokenExprieIn * 1000
instance.setToken({ token, tokenExpireTime }) // 存token到localStorage
isFlag = false //一旦刷新成功,再重置回去
config.headers['token'] = token // 请求头重新赋值token
return config
}).then((token) => {
console.log('刷新token成功,执行队列')
//在token刷新成功后,才会继续执行队列
requestsArr.forEach(item => item(token))
// 执行完成后,清空队列
requestsArr = []
}).catch(res => {
console.error('token刷新error', res)
})
}else{ //但是这样写还是会出问题,多次发起请求的顺序会乱,所以这边加个else
//这里第一个请求进不来
const retryOriginalRequest = new Promise((resolve) => {
requestsArr.push((token) => {
config.headers['token'] = token
resolve(config)
})
})
return retryOriginalRequest
}
方案二
思路:
先不管token是否过期,直接请求,如果返回401(后端定义的,我这里401是token过期),去刷新token,再重新调之前的接口
我个人并不推荐这种方法,先不更新这个了。