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 相关文章推荐
Js sort排序使用方法
Oct 17 Javascript
jQuery操作 input type=checkbox的实现代码
Jun 14 Javascript
Javascript和HTML5利用canvas构建Web五子棋游戏实现算法
Jul 17 Javascript
基于JavaScript实现根据手机定位获取当前具体位置(X省X市X县X街道X号)
Dec 29 Javascript
网页前端登录js按Enter回车键实现登陆的两种方法
May 10 Javascript
浅谈JS之tagNaem和nodeName
Sep 13 Javascript
javascript设置文本框光标的方法实例小结
Nov 04 Javascript
jQuery zTree树插件简单使用教程
Jan 10 Javascript
D3.js进阶系列之CSV表格文件的读取详解
Jun 06 Javascript
js生成word中图片处理方法
Jan 06 Javascript
layui导出所有数据的例子
Sep 10 Javascript
Javascript作用域和作用域链原理解析
Mar 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
用php实现百度网盘图片直链的代码分享
2012/11/01 PHP
PHP正则表达式处理函数(PCRE 函数)实例小结
2019/05/09 PHP
在TP5数据库中四个字段实现无限分类的示例
2019/10/18 PHP
js自执行函数的几种不同写法的比较
2012/08/16 Javascript
JS随即打乱数组实现代码
2012/12/03 Javascript
JS限制Textarea文本域字符个数的具体实现
2013/08/02 Javascript
form表单只提交数据而不进行页面跳转的解决方案
2013/09/18 Javascript
Jquery 在页面加载后执行的几种方式
2014/03/14 Javascript
分享我的jquery实现下拉菜单心的
2015/11/29 Javascript
Vuejs第六篇之Vuejs与form元素实例解析
2016/09/05 Javascript
微信小程序 实战程序简易新闻的制作
2017/01/09 Javascript
详解React中的组件通信问题
2017/07/31 Javascript
vue 之 .sync 修饰符示例详解
2018/04/21 Javascript
给localStorage设置一个过期时间的方法分享
2018/11/06 Javascript
微信小程序下拉框功能的实例代码
2018/11/06 Javascript
jQuery点击页面其他部分隐藏下拉菜单功能
2018/11/27 jQuery
基于vue-cli、elementUI的Vue超简单入门小例子(推荐)
2019/04/17 Javascript
基于js判断浏览器是否支持webGL
2020/04/18 Javascript
Vue指令实现OutClick的示例
2020/11/16 Javascript
Python获取远程文件大小的函数代码分享
2014/05/13 Python
详解Python 模拟实现生产者消费者模式的实例
2017/08/10 Python
对python中的six.moves模块的下载函数urlretrieve详解
2018/12/19 Python
Python math库 ln(x)运算的实现及原理
2019/07/17 Python
简单了解python gevent 协程使用及作用
2019/07/22 Python
python读取Kafka实例
2019/12/23 Python
python数字类型math库原理解析
2020/03/02 Python
python输出国际象棋棋盘的实例分享
2020/11/26 Python
HTML5注册页面示例代码
2014/03/27 HTML / CSS
巴西男士个人护理产品商店:SHOP4MEN
2017/08/07 全球购物
Set里的元素是不能重复的,那么用什么方法来区分重复与否呢? 是用==还是equals()? 它们有何区别?
2014/07/27 面试题
服装厂厂长岗位职责
2013/12/27 职场文书
高中政治教学反思
2014/01/18 职场文书
大学毕业谢师宴致辞
2015/07/27 职场文书
卫生主题班会
2015/08/14 职场文书
2019交通安全宣传标语集锦!
2019/06/28 职场文书
Java基础之详解HashSet的使用方法
2021/06/30 Java/Android