axios如何利用promise无痛革新token

打印 上一主题 下一主题

主题 1713|帖子 1713|积分 5139

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
目录
需求
需求解析
实现思路
方法一:
方法二:
两种方法对比
实现
封装axios根本骨架
instance.interceptors.response.use拦截实现
问题和优化
如何防止多次革新token
同时发起两个或以上的请求时,其他接口如何重试
最后完备代码


 
需求

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

当用户发起一个请求时,判断token是否已过期,若已过期则先调refreshToken接口,拿到新的token后再继续执行之前的请求。
这个问题的难点在于:当同时发起多个请求,而革新token的接口还没返回,此时其他请求该如何处置惩罚?接下来会循规蹈矩地分享一下整个过程。
实现思路

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

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

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

方法一


  • 优点: 在请求前拦截,能节省请求,省流量。
  • 缺点: 需要后端额外提供一个token过期时间的字段;利用了本地时间判断,若本地时间被篡改,特别是本地时间比服务器时间慢时,拦截会失败。
   PS:token有效时间发起是时间段,类似缓存的MaxAge,而不要是绝对时间。当服务器和本地时间不一致时,绝对时间会有问题。  方法二


  • 优点:不需额外的token过期字段,不需判断时间。
  • 缺点: 会消耗多一次请求,耗流量。
综上,方法一和二优缺点是互补的,方法一有校验失败的风险(本地时间被篡改时,固然一般没有效户闲的蛋疼去改本地时间的啦),方法二更简单粗暴,等知道服务器已经过期了再重试一次,只是会耗多一个请求。
在这里博主选择了 方法二
实现

这里会利用axios来实现,方法一是请求前拦截,所以会利用axios.interceptors.request.use()这个方法;
而方法二是请求后拦截,所以会利用axios.interceptors.response.use()方法。
封装axios根本骨架

起首说明一下,项目中的token是存在localStorage中的。request.js根本骨架:
  1. import axios from 'axios'
  2. // 从localStorage中获取token
  3. function getLocalToken () {
  4.     const token = window.localStorage.getItem('token')
  5.     return token
  6. }
  7. // 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
  8. instance.setToken = (token) => {
  9.   instance.defaults.headers['X-Token'] = token
  10.   window.localStorage.setItem('token', token)
  11. }
  12. // 创建一个axios实例
  13. const instance = axios.create({
  14.   baseURL: '/api',
  15.   timeout: 300000,
  16.   headers: {
  17.     'Content-Type': 'application/json',
  18.     'X-Token': getLocalToken() // headers塞token
  19.   }
  20. })
  21. // 拦截返回的数据
  22. instance.interceptors.response.use(response => {
  23.   // 接下来会在这里进行token过期的逻辑处理
  24.   return response
  25. }, error => {
  26.   return Promise.reject(error)
  27. })
  28. export default instance
复制代码
这个是项目中一般的axios实例的封装,创建实例时,将本地已有的token放进header,然后export出去供调用。接下来就是如何拦截返回的数据啦。
instance.interceptors.response.use拦截实现

后端接口一般会有一个约定好的数据布局,如:
  1. {code: 1234, message: 'token过期', data: {}}
复制代码
如我这里,后端约定当code === 1234时表现token过期了,此时就要求革新token。
  1. instance.interceptors.response.use(response => {
  2.   const { code } = response.data
  3.   if (code === 1234) {
  4.     // 说明token过期了,刷新token
  5.     return refreshToken().then(res => {
  6.       // 刷新token成功,将最新的token更新到header中,同时保存在localStorage中
  7.       const { token } = res.data
  8.       instance.setToken(token)
  9.       // 获取当前失败的请求
  10.       const config = response.config
  11.       // 重置一下配置
  12.       config.headers['X-Token'] = token
  13.       config.baseURL = '' // url已经带上了/api,避免出现/api/api的情况
  14.       // 重试当前请求并返回promise
  15.       return instance(config)
  16.     }).catch(res => {
  17.       console.error('refreshtoken error =>', res)
  18.       //刷新token失败,神仙也救不了了,跳转到首页重新登录吧
  19.       window.location.href = '/'
  20.     })
  21.   }
  22.   return response
  23. }, error => {
  24.   return Promise.reject(error)
  25. })
  26. function refreshToken () {
  27.     // instance是当前request.js中已创建的axios实例
  28.     return instance.post('/refreshtoken').then(res => res.data)
  29. }
复制代码
这里需要额外注意的是,response.config就是原请求的配置,但这个是已经处置惩罚过了的,config.url已经带上了baseUrl,因此重试时需要去掉,同时token也是旧的,需要革新下。
以上就根本做到了无痛革新token,当token正常时,正常返回,当token已过期,则axios内部进行一次革新token和重试。对调用者来说,axios内部的革新token是一个黑盒,是无感知的,因此需求已经做到了。
问题和优化

上面的代码还是存在一些问题的,没有思量到多次请求的问题,因此需要进一步优化。
如何防止多次革新token

假如refreshToken接口还没返回,此时再有一个过期的请求进来,上面的代码就会再一次执行refreshToken,这就会导致多次执行革新token的接口,因此需要防止这个问题。我们可以在request.js中用一个flag来标记当前是否正在革新token的状态,假如正在革新则不再调用革新token的接口。
  1. // 是否正在刷新的标记
  2. let isRefreshing = false
  3. instance.interceptors.response.use(response => {
  4.   const { code } = response.data
  5.   if (code === 1234) {
  6.     if (!isRefreshing) {
  7.       isRefreshing = true
  8.       return refreshToken().then(res => {
  9.         const { token } = res.data
  10.         instance.setToken(token)
  11.         const config = response.config
  12.         config.headers['X-Token'] = token
  13.         config.baseURL = ''
  14.         return instance(config)
  15.       }).catch(res => {
  16.         console.error('refreshtoken error =>', res)
  17.         window.location.href = '/'
  18.       }).finally(() => {
  19.         isRefreshing = false
  20.       })
  21.     }
  22.   }
  23.   return response
  24. }, error => {
  25.   return Promise.reject(error)
  26. })
复制代码
这样子就可以制止在革新token时再进入方法了。但是这种做法是相当于把其他失败的接口给舍弃了,假犹如时发起两个请求,且险些同时返回,第一个请求肯定是进入了refreshToken后再重试,而第二个请求则被抛弃了,仍是返回失败,所以接下来还得解决其他接口的重试问题。
同时发起两个或以上的请求时,其他接口如何重试

两个接口险些同时发起和返回,第一个接口会进入革新token后重试的流程,而第二个接口需要先存起来,然后等革新token后再重试。同样,假如同时发起三个请求,此时需要缓存后两个接口,等革新token后再重试。由于接口都是异步的,处置惩罚起来会有点贫苦。
当第二个过期的请求进来,token正在革新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直比及革新token后再逐个重试清空请求队列。
那么如何做到让这个请求处于等待中呢?为相识决这个问题,我们得借助Promise。将请求存进队列中后,同时返回一个Promise,让这个Promise一直处于Pending状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当革新请求的接口返返来后,我们再调用resolve,逐个重试。最终代码:
  1. // 是否正在刷新的标记
  2. let isRefreshing = false
  3. // 重试队列,每一项将是一个待执行的函数形式
  4. let requests = []
  5. instance.interceptors.response.use(response => {
  6.   const { code } = response.data
  7.   if (code === 1234) {
  8.     const config = response.config
  9.     if (!isRefreshing) {
  10.       isRefreshing = true
  11.       return refreshToken().then(res => {
  12.         const { token } = res.data
  13.         instance.setToken(token)
  14.         config.headers['X-Token'] = token
  15.         config.baseURL = ''
  16.         // 已经刷新了token,将所有队列中的请求进行重试
  17.         requests.forEach(cb => cb(token))
  18.         // 重试完了别忘了清空这个队列(掘金评论区同学指点)
  19.         requests = []
  20.         return instance(config)
  21.       }).catch(res => {
  22.         console.error('refreshtoken error =>', res)
  23.         window.location.href = '/'
  24.       }).finally(() => {
  25.         isRefreshing = false
  26.       })
  27.     } else {
  28.       // 正在刷新token,返回一个未执行resolve的promise
  29.       return new Promise((resolve) => {
  30.         // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
  31.         requests.push((token) => {
  32.           config.baseURL = ''
  33.           config.headers['X-Token'] = token
  34.           resolve(instance(config))
  35.         })
  36.       })
  37.     }
  38.   }
  39.   return response
  40. }, error => {
  41.   return Promise.reject(error)
  42. })
复制代码
这里可能比较难明确的是requests这个队列中保存的是一个函数,这是为了让resolve不执行,先存起来,等革新token后更方便调用这个函数使得resolve执行。至此,问题应该都解决了
最后完备代码

  1. import axios from 'axios'
  2. // 从localStorage中获取token
  3. function getLocalToken () {
  4.     const token = window.localStorage.getItem('token')
  5.     return token
  6. }
  7. // 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
  8. instance.setToken = (token) => {
  9.   instance.defaults.headers['X-Token'] = token
  10.   window.localStorage.setItem('token', token)
  11. }
  12. function refreshToken () {
  13.     // instance是当前request.js中已创建的axios实例
  14.     return instance.post('/refreshtoken').then(res => res.data)
  15. }
  16. // 创建一个axios实例
  17. const instance = axios.create({
  18.   baseURL: '/api',
  19.   timeout: 300000,
  20.   headers: {
  21.     'Content-Type': 'application/json',
  22.     'X-Token': getLocalToken() // headers塞token
  23.   }
  24. })
  25. // 是否正在刷新的标记
  26. let isRefreshing = false
  27. // 重试队列,每一项将是一个待执行的函数形式
  28. let requests = []
  29. instance.interceptors.response.use(response => {
  30.   const { code } = response.data
  31.   if (code === 1234) {
  32.     const config = response.config
  33.     if (!isRefreshing) {
  34.       isRefreshing = true
  35.       return refreshToken().then(res => {
  36.         const { token } = res.data
  37.         instance.setToken(token)
  38.         config.headers['X-Token'] = token
  39.         config.baseURL = ''
  40.         // 已经刷新了token,将所有队列中的请求进行重试
  41.         requests.forEach(cb => cb(token))
  42.         requests = []
  43.         return instance(config)
  44.       }).catch(res => {
  45.         console.error('refreshtoken error =>', res)
  46.         window.location.href = '/'
  47.       }).finally(() => {
  48.         isRefreshing = false
  49.       })
  50.     } else {
  51.       // 正在刷新token,将返回一个未执行resolve的promise
  52.       return new Promise((resolve) => {
  53.         // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
  54.         requests.push((token) => {
  55.           config.baseURL = ''
  56.           config.headers['X-Token'] = token
  57.           resolve(instance(config))
  58.         })
  59.       })
  60.     }
  61.   }
  62.   return response
  63. }, error => {
  64.   return Promise.reject(error)
  65. })
  66. export default instance
复制代码


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

曂沅仴駦

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表