axios如何利用promise无痛刷新token10bet:?

来源:http://www.chinese-glasses.com 作者:Web前端 人气:191 发布时间:2020-03-24
摘要:在请求发起前拦截每个请求,判断token的有效时间是否已经过期,若已过期,则将请求挂起,先刷新token后再继续请求。 封装axios基本骨架 与前文略微不同的是,由于 方法二 不需要用到

在请求发起前拦截每个请求,判断token的有效时间是否已经过期,若已过期,则将请求挂起,先刷新token后再继续请求。

封装axios基本骨架

与前文略微不同的是,由于方法二不需要用到过期时间,所以前文localStorage中只存了token一个字符串,而方法一这里需要用到过期时间了,所以得存多一个数据,因此localStorage中存的是Object类型的数据,从localStorage中取值出来需要JSON.parse一下,为了防止发生错误所以尽量使用try...catch。

两个接口几乎同时发起和返回,第一个接口会进入刷新token后重试的流程,而第二个接口需要先存起来,然后等刷新token后再重试。同样,如果同时发起三个请求,此时需要缓存后两个接口,等刷新token后再重试。由于接口都是异步的,处理起来会有点麻烦。

这里做了一点改动,注意到refreshToken()这一句前面去掉了return,而是改为了在后面return retryOriginalRequest,即当发现有请求是过期的就存进requests数组,等refreshToken结束后再执行requests队列,这是为了不影响原来的请求执行次序。我们假设同时有请求1,请求2,请求3依次同时进来,我们希望是请求1发现过期,refreshToken后再依次执行请求1,请求2,请求3。按之前return refreshToken()的写法,会大概写成这样

优点: 在请求前拦截,能节省请求,省流量。 缺点: 需要后端额外提供一个token过期时间的字段;使用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败。

队列里面只有请求2和请求3,代码看起来应该是return了请求1后,再在finally执行队列的,但实际的执行顺序会变成请求2,请求3,请求1,即请求1变成了最后一个执行的,会改变执行顺序。

如何防止多次刷新token

所以博主换了个思路,无论是哪个请求进入了过期流程,我们都将请求放到队列中,都return一个未resolve的Promise,等刷新token结束后再一一清算,这样就可以保证请求1,请求2,请求3这样按原来顺序执行了。

// 是否正在刷新的标记let isRefreshing = falseinstance.interceptors.response.use(response => { const { code } = response.data if  { if  { isRefreshing = true return refreshToken().then(res => { const { token } = res.data instance.setToken const config = response.config config.headers['X-Token'] = token config.baseURL = '' return instance.catch(res => { console.error('refreshtoken error =>', res) window.location.href = '/' }).finally => { isRefreshing = false }) } } return response}, error => { return Promise.reject

实现基本骨架

// 是否正在刷新的标记let isRefreshing = false// 重试队列,每一项将是一个待执行的函数形式const requests = []instance.interceptors.response.use(response => { const { code } = response.data if  { const config = response.config if  { isRefreshing = true return refreshToken().then(res => { const { token } = res.data instance.setToken config.headers['X-Token'] = token config.baseURL = '' // 已经刷新了token,将所有队列中的请求进行重试 requests.forEach return instance.catch(res => { console.error('refreshtoken error =>', res) window.location.href = '/' }).finally => { isRefreshing = false }) } else { // 正在刷新token,返回一个未执行resolve的promise return new Promise => { // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行 requests.push => { config.baseURL = '' config.headers['X-Token'] = token resolve }) } } return response}, error => { return Promise.reject

因此需要将刷新token和登录这两种情况排除出去,登录和刷新token都不需要判断是否过期的拦截,我们可以通过config.url来判断是哪个接口:

而方法二是请求后拦截,所以会使用axios.interceptors.response.use()方法。

首先不需要想得太复杂,先不考虑多个请求同时进来的情况,咱从最常见的场景入手:从localStorage拿到上一次存储的过期时间,判断是否已经到了过期时间,是就立即刷新token然后再发起请求。

这里会使用axios来实现,方法一是请求前拦截,所以会使用axios.interceptors.request.use()这个方法;

不在请求前拦截,而是拦截返回后的数据。先发起请求,接口返回过期后,先刷新token,再进行一次重试。

以上就基本做到了无痛刷新token,当token正常时,正常返回,当token已过期,则axios内部进行一次刷新token和重试。对调用者来说,axios内部的刷新token是一个黑盒,是无感知的,因此需求已经做到了。

我们已经知道了当前已经过期或者正在刷新token,此时再有请求发起,就应该让后面的这些请求等一等,等到refreshToken结束后再真正发起,所以需要用到一个Promise来让它一直等。而后面的所有请求,我们将它们存放到一个requests的队列中,等刷新token后再依次resolve。

由于后端返回了token的有效时间,可以有两种方法:

之前说到登录或刷新token的接口返回的是一个单位为秒的时间段tokenExpireIn,而我们存到localStorage中的是已经是一个基于当前时间和有效时间段算出的最终时间tokenExpireTime,是一个绝对时间,比如当前时间是12点,有效时间是3600秒(1个小时),则存到localStorage的过期时间是13点的时间戳,这样可以少存一个当前时间的字段到localStorage中,使用时只需要判断该绝对时间即可。instance.interceptors.request.use中返回一个Promise,就可以使得该请求是先执行refreshToken后再return config的,才能保证先刷新token后再真正发起请求。

这样子就可以避免在刷新token时再进入方法了。但是这种做法是相当于把其他失败的接口给舍弃了,假如同时发起两个请求,且几乎同时返回,第一个请求肯定是进入了refreshToken后再重试,而第二个请求则被丢弃了,仍是返回失败,所以接下来还得解决其他接口的重试问题。

10bet,接下来就是要考虑复杂一点的问题了

如果refreshToken接口还没返回,此时再有一个过期的请求进来,上面的代码就会再一次执行refreshToken,这就会导致多次执行刷新token的接口,因此需要防止这个问题。我们可以在request.js中用一个flag来标记当前是否正在刷新token的状态,如果正在刷新则不再调用刷新token的接口。

原文:

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。

instance.interceptors.request.use((config) = { const tokenObj = getToken() // 添加请求头 config.headers['X-Token'] = tokenObj.token // 登录接口和刷新token接口绕过 if (config.url.indexOf('/refreshToken') = 0 || config.url.indexOf('/login') = 0) { return config } if (tokenObj.token  tokenObj.tokenExpireTime) { const now = Date.now() if (now = tokenObj.tokenExpireTime) { // 立即刷新token if (!isRefreshing) { console.log('刷新token ing') isRefreshing = true refreshToken().then(res = { const { token, tokenExprieIn } = res.data const tokenExpireTime = now + tokenExprieIn * 1000 instance.setToken({ token, tokenExpireTime }) isRefreshing = false return token }).then((token) = { console.log('刷新token成功,执行队列') requests.forEach(cb = cb(token)) // 执行完成后,清空队列 requests = [] }).catch(res = { console.error('refresh token error: ', res) }) } const retryOriginalRequest = new Promise((resolve) = { requests.push((token) = { // 因为config中的token是旧的,所以刷新token后要将新token传进来 config.headers['X-Token'] = token resolve(config) }) }) return retryOriginalRequest } } return config}, (error) = { // Do something with request error return Promise.reject(error)})

这里可能比较难理解的是requests这个队列中保存的是一个函数,这是为了让resolve不执行,先存起来,等刷新token后更方便调用这个函数使得resolve执行。至此,问题应该都解决了。

在请求前进行拦截,我们主要会使用axios.interceptors.request.use()这个方法。照例先封装个request.js的基本骨架:

两种方法对比

这里有两个需要注意的地方:

import axios from 'axios'// 从localStorage中获取tokenfunction getLocalToken () { const token = window.localStorage.getItem return token}// 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中instance.setToken =  => { instance.defaults.headers['X-Token'] = token window.localStorage.setItem}// 创建一个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.rejectexport default instance
function refreshToken () { // instance是当前request.js中已创建的axios实例 return instance.post('/refreshtoken').then(res = res.data)}instance.interceptors.request.use((config) = { const tokenObj = getToken() // 为每个请求添加token请求头 config.headers['X-Token'] = tokenObj.token if (tokenObj.token  tokenObj.tokenExpireTime) { const now = Date.now() if (now = tokenObj.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 console.log('刷新成功, return config即是恢复当前请求') config.headers['X-Token'] = token // 将最新的token放到请求头 return config }).catch(res = { console.error('refresh token error: ', res) }) } } return config}, (error) = { // Do something with request error return Promise.reject(error)})

后端接口一般会有一个约定好的数据结构,如:

当几乎同时进来两个请求,为了避免多次执行refreshToken,需要引入一个isRefreshing的进行标记:

最近遇到个需求:前端登录后,后端返回token和token有效时间,当token过期时要求用旧token去获取新的token,前端需要做到无痛刷新token,即请求刷新token时要做到用户无感知。

instance.interceptors.request.use((config) = { const tokenObj = getToken() // 为每个请求添加token请求头 config.headers['X-Token'] = tokenObj.token // 登录接口和刷新token接口绕过 if (config.url.indexOf('/refreshToken') = 0 || config.url.indexOf('/login') = 0) { return config } if (tokenObj.token  tokenObj.tokenExpireTime) { const now = Date.now() if (now = tokenObj.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 console.log('刷新成功, return config即是恢复当前请求') config.headers['X-Token'] = token // 将最新的token放到请求头 return config }).catch(res = { console.error('refresh token error: ', res) }) } } return config}, (error) = { // Do something with request error return Promise.reject(error)})

方法二:不在请求前拦截,而是拦截返回后的数据。先发起请求,接口返回过期后,先刷新token,再进行一次重试。

import axios from 'axios'// 从localStorage中获取token,token存的是object信息,有tokenExpireTime和token两个字段function getToken () { let tokenObj = {} try { tokenObj = storage.get('token') tokenObj = tokenObj ? JSON.parse(tokenObj) : {} } catch { console.error('get token from localStorage error') } return tokenObj}// 给实例添加一个setToken方法,用于登录后方便将最新token动态添加到header,同时将token保存在localStorage中instance.setToken = (obj) = { instance.defaults.headers['X-Token'] = obj.token window.localStorage.setItem('token', JSON.stringify(obj)) // 注意这里需要变成字符串后才能放到localStorage中}// 创建一个axios实例const instance = axios.create({ baseURL: '/api', timeout: 300000, headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }})// 请求发起前拦截instance.interceptors.request.use((config) = { const tokenObj = getToken() // 为每个请求添加token请求头 config.headers['X-Token'] = tokenObj.token // **接下来主要拦截的实现就在这里** return config}, (error) = { // Do something with request error return Promise.reject(error)})// 请求返回后拦截instance.interceptors.response.use(response = { const { code } = response.data if (code === 1234) { // token过期了,直接跳转到登录页 window.location.href = '/' } return response}, error = { console.log('catch', error) return Promise.reject(error)})export default instance

优点:不需额外的token过期字段,不需判断时间。缺点: 会消耗多一次请求,耗流量。

多个请求时存到队列中等刷新token后再发起

综上,方法一和二优缺点是互补的,方法一有校验失败的风险(本地时间被篡改时,当然一般没有用户闲的蛋疼去改本地时间的啦),方法二更简单粗暴,等知道服务器已经过期了再重试一次,只是会耗多一个请求。在这里博主选择了 方法二。

本文由10bet发布于Web前端,转载请注明出处:axios如何利用promise无痛刷新token10bet:?

关键词:

最火资讯