axios如何利用promise无痛刷新token的实现方法


Posted in Javascript onAugust 27, 2019

需求

最近遇到个需求:前端登录后,后端返回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基本骨架:

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出去供调用。接下来就是如何拦截返回的数据啦。

instance.interceptors.response.use拦截实现

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

{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是一个黑盒,是无感知的,因此需求已经做到了。

问题和优化

上面的代码还是存在一些问题的,没有考虑到多次请求的问题,因此需要进一步优化。

如何防止多次刷新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
// 重试队列,每一项将是一个待执行的函数形式
const 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))
  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
// 重试队列,每一项将是一个待执行的函数形式
const 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))
  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

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

Javascript 相关文章推荐
javascript下有关dom以及xml节点访问兼容问题
Nov 26 Javascript
javascript json2 使用方法
Mar 16 Javascript
页面按钮禁用与解除禁用的方法
Feb 19 Javascript
js实现的类似于asp数据字典的数据类型代码实例
Sep 03 Javascript
jQuery中mouseover事件用法实例
Dec 26 Javascript
基于CSS3和jQuery实现跟随鼠标方位的Hover特效
Jul 25 Javascript
ajax前台后台跨域请求处理方式
Feb 08 Javascript
webpack的CSS加载器的使用
Sep 11 Javascript
vue.js编译时给生成的文件增加版本号
Sep 17 Javascript
实例讲解JS中pop使用方法
Jan 27 Javascript
Vue性能优化的方法
Jul 30 Javascript
浅谈vue中get请求解决传输数据是数组格式的问题
Aug 03 Javascript
解决vue打包后刷新页面报错:Unexpected token
Aug 27 #Javascript
JS用最简单的方法实现四舍五入
Aug 27 #Javascript
微信小程序模板消息推送的两种实现方式
Aug 27 #Javascript
vue实现codemirror代码编辑器中的SQL代码格式化功能
Aug 27 #Javascript
详解微信小程序开发之formId使用(模板消息)
Aug 27 #Javascript
在vue项目中使用codemirror插件实现代码编辑器功能
Aug 27 #Javascript
vue使用codemirror的两种用法
Aug 27 #Javascript
You might like
了解咖啡雨林联盟认证 什么是雨林认证 雨林认证是什么意思
2021/03/05 新手入门
php下尝试使用GraphicsMagick的缩略图功能
2011/01/01 PHP
使用phpQuery获取数组的实例
2017/03/13 PHP
浅析PHP中的 inet_pton 网络函数
2019/12/16 PHP
记Laravel调用Gin接口调用formData上传文件的实现方法
2019/12/12 PHP
js实现页面打印功能实例代码(附去页眉页脚功能代码)
2009/12/15 Javascript
关于跨站脚本攻击问题
2011/12/22 Javascript
JavaScript Scoping and Hoisting 翻译
2012/07/03 Javascript
js网页右下角提示框实例
2014/10/14 Javascript
纯HTML5制作围住神经猫游戏-附源码下载
2015/08/23 Javascript
Jquery 效果使用详解
2015/11/23 Javascript
基于jquery实现ajax无刷新评论
2020/08/19 Javascript
总结Javascript中数组各种去重的方法
2016/10/04 Javascript
JS实现颜色动态淡化效果
2017/03/06 Javascript
详解使用vue脚手架工具搭建vue-webpack项目
2017/05/10 Javascript
Easyui在treegrid添加控件的实现方法
2017/06/23 Javascript
vue1.0和vue2.0的watch监听事件写法详解
2018/09/11 Javascript
vue-cli3.0如何使用CDN区分开发、生产、预发布环境
2018/11/22 Javascript
[05:04]完美世界携手游戏风云打造 卡尔工作室地图界面篇
2013/04/23 DOTA
[02:04]2018DOTA2亚洲邀请赛Secret赛前采访
2018/04/03 DOTA
Python多进程并发(multiprocessing)用法实例详解
2015/06/02 Python
分享一下如何编写高效且优雅的 Python 代码
2017/09/07 Python
Python实现XML文件解析的示例代码
2018/02/05 Python
对Python 中矩阵或者数组相减的法则详解
2019/08/26 Python
python正则表达式 匹配反斜杠的操作方法
2020/08/07 Python
HTML5 Canvas玩转酷炫大波浪进度图效果实例(附demo)
2016/12/14 HTML / CSS
不同浏览器创建XMLHttpRequest方法有什么不同
2014/11/17 面试题
Python中pass语句的作用是什么
2016/06/01 面试题
工程造价自荐信
2013/10/09 职场文书
借款协议书范本
2014/04/22 职场文书
我的中国梦演讲稿400字
2014/08/19 职场文书
寒暑假实习证明书模板
2014/11/29 职场文书
2014年高中教师工作总结
2014/12/19 职场文书
单位租车协议书
2015/01/29 职场文书
新学期小学班主任工作计划
2019/06/21 职场文书
React更新渲染原理深入分析
2022/12/24 Javascript