最近遇到个需求:前端登陆后,后端返回token
和token有效时间
,当token过时时要求用旧token去获取新的token,前端须要作到无痛刷新token
,即请求刷新token时要作到用户无感知。前端
当用户发起一个请求时,判断token是否已过时,若已过时则先调refreshToken
接口,拿到新的token后再继续执行以前的请求。ios
这个问题的难点在于:当同时发起多个请求,而刷新token的接口还没返回,此时其余请求该如何处理?接下来会按部就班地分享一下整个过程。git
因为后端返回了token的有效时间,能够有两种方法:github
在请求发起前拦截每一个请求,判断token的有效时间是否已通过期,若已过时,则将请求挂起,先刷新token后再继续请求。json
不在请求前拦截,而是拦截返回后的数据。先发起请求,接口返回过时后,先刷新token,再进行一次重试。axios
方法一后端
PS:token有效时间建议是时间段,相似缓存的MaxAge,而不要是绝对时间。当服务器和本地时间不一致时,绝对时间会有问题。api
方法二数组
综上,方法一和二优缺点是互补的,方法一有校验失败的风险(本地时间被篡改时,固然通常没有用户闲的蛋疼去改本地时间的啦),方法二更简单粗暴,等知道服务器已通过期了再重试一次,只是会耗多一个请求。promise
在这里博主选择了 方法二。
这里会使用axios来实现,方法一是请求前拦截,因此会使用axios.interceptors.request.use()
这个方法;
而方法二是请求后拦截,因此会使用axios.interceptors.response.use()
方法。
首先说明一下,项目中的token是存在localStorage
中的。request.js
基本骨架:
import axios from 'axios'
// 从localStorage中获取token
function getLocalToken () {
const token = window.localStorage.getItem('token')
return token
}
// 给实例添加一个setToken方法,用于登陆后将最新token动态添加到header,同时将token保存在localStorage中
instance.setToken = (token) => {
instance.defaults.headers['X-Token'] = token
window.localStorage.setItem('token', token)
}
// 建立一个axios实例
const instance = axios.create({
baseURL: '/api',
timeout: 300000,
headers: {
'Content-Type': 'application/json',
'X-Token': getLocalToken() // headers塞token
}
})
// 拦截返回的数据
instance.interceptors.response.use(response => {
// 接下来会在这里进行token过时的逻辑处理
return response
}, error => {
return Promise.reject(error)
})
export default instance
复制代码
这个是项目中通常的axios实例的封装,建立实例时,将本地已有的token放进header,而后export出去供调用。接下来就是如何拦截返回的数据啦。
后端接口通常会有一个约定好的数据结构,如:
{code: 1234, message: 'token过时', data: {}}
复制代码
如我这里,后端约定当code === 1234
时表示token过时了,此时就要求刷新token。
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 1234) {
// 说明token过时了,刷新token
return refreshToken().then(res => {
// 刷新token成功,将最新的token更新到header中,同时保存在localStorage中
const { token } = res.data
instance.setToken(token)
// 获取当前失败的请求
const config = response.config
// 重置一下配置
config.headers['X-Token'] = token
config.baseURL = '' // url已经带上了/api,避免出现/api/api的状况
// 重试当前请求并返回promise
return instance(config)
}).catch(res => {
console.error('refreshtoken error =>', res)
//刷新token失败,神仙也救不了了,跳转到首页从新登陆吧
window.location.href = '/'
})
}
return response
}, error => {
return Promise.reject(error)
})
function refreshToken () {
// instance是当前request.js中已建立的axios实例
return instance.post('/refreshtoken').then(res => res.data)
}
复制代码
这里须要额外注意的是,response.config
就是原请求的配置,但这个是已经处理过了的,config.url
已经带上了baseUrl
,所以重试时须要去掉,同时token也是旧的,须要刷新下。
以上就基本作到了无痛刷新token,当token正常时,正常返回,当token已过时,则axios内部进行一次刷新token和重试。对调用者来讲,axios内部的刷新token是一个黑盒,是无感知的,所以需求已经作到了。
上面的代码仍是存在一些问题的,没有考虑到屡次请求的问题,所以须要进一步优化。
若是refreshToken接口还没返回,此时再有一个过时的请求进来,上面的代码就会再一次执行refreshToken,这就会致使屡次执行刷新token的接口,所以须要防止这个问题。咱们能够在request.js
中用一个flag
来标记当前是否正在刷新token的状态,若是正在刷新则再也不调用刷新token的接口。
// 是否正在刷新的标记
let isRefreshing = false
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 1234) {
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res => {
const { token } = res.data
instance.setToken(token)
const config = response.config
config.headers['X-Token'] = token
config.baseURL = ''
return instance(config)
}).catch(res => {
console.error('refreshtoken error =>', res)
window.location.href = '/'
}).finally(() => {
isRefreshing = false
})
}
}
return response
}, error => {
return Promise.reject(error)
})
复制代码
这样子就能够避免在刷新token时再进入方法了。可是这种作法是至关于把其余失败的接口给舍弃了,假如同时发起两个请求,且几乎同时返回,第一个请求确定是进入了refreshToken后再重试,而第二个请求则被丢弃了,还是返回失败,因此接下来还得解决其余接口的重试问题。
两个接口几乎同时发起和返回,第一个接口会进入刷新token后重试的流程,而第二个接口须要先存起来,而后等刷新token后再重试。一样,若是同时发起三个请求,此时须要缓存后两个接口,等刷新token后再重试。因为接口都是异步的,处理起来会有点麻烦。
当第二个过时的请求进来,token正在刷新,咱们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。 那么如何作到让这个请求处于等待中呢?为了解决这个问题,咱们得借助Promise
。将请求存进队列中后,同时返回一个Promise
,让这个Promise
一直处于Pending
状态(即不调用resolve),此时这个请求就会一直等啊等,只要咱们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,咱们再调用resolve,逐个重试。最终代码:
// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 1234) {
const config = response.config
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res => {
const { token } = res.data
instance.setToken(token)
config.headers['X-Token'] = token
config.baseURL = ''
// 已经刷新了token,将全部队列中的请求进行重试
requests.forEach(cb => cb(token))
// 重试完了别忘了清空这个队列(掘金评论区同窗指点)
requests = []
return instance(config)
}).catch(res => {
console.error('refreshtoken error =>', res)
window.location.href = '/'
}).finally(() => {
isRefreshing = false
})
} else {
// 正在刷新token,返回一个未执行resolve的promise
return new Promise((resolve) => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
requests.push((token) => {
config.baseURL = ''
config.headers['X-Token'] = token
resolve(instance(config))
})
})
}
}
return response
}, error => {
return Promise.reject(error)
})
复制代码
这里可能比较难理解的是requests
这个队列中保存的是一个函数,这是为了让resolve不执行,先存起来,等刷新token后更方便调用这个函数使得resolve执行。至此,问题应该都解决了。
import axios from 'axios'
// 从localStorage中获取token
function getLocalToken () {
const token = window.localStorage.getItem('token')
return token
}
// 给实例添加一个setToken方法,用于登陆后将最新token动态添加到header,同时将token保存在localStorage中
instance.setToken = (token) => {
instance.defaults.headers['X-Token'] = token
window.localStorage.setItem('token', token)
}
function refreshToken () {
// instance是当前request.js中已建立的axios实例
return instance.post('/refreshtoken').then(res => res.data)
}
// 建立一个axios实例
const instance = axios.create({
baseURL: '/api',
timeout: 300000,
headers: {
'Content-Type': 'application/json',
'X-Token': getLocalToken() // headers塞token
}
})
// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []
instance.interceptors.response.use(response => {
const { code } = response.data
if (code === 1234) {
const config = response.config
if (!isRefreshing) {
isRefreshing = true
return refreshToken().then(res => {
const { token } = res.data
instance.setToken(token)
config.headers['X-Token'] = token
config.baseURL = ''
// 已经刷新了token,将全部队列中的请求进行重试
requests.forEach(cb => cb(token))
requests = []
return instance(config)
}).catch(res => {
console.error('refreshtoken error =>', res)
window.location.href = '/'
}).finally(() => {
isRefreshing = false
})
} else {
// 正在刷新token,将返回一个未执行resolve的promise
return new Promise((resolve) => {
// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
requests.push((token) => {
config.baseURL = ''
config.headers['X-Token'] = token
resolve(instance(config))
})
})
}
}
return response
}, error => {
return Promise.reject(error)
})
export default instance
复制代码
但愿对你们有帮助。感谢看到最后,感谢点赞^_^。