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 相关文章推荐
各种页面定时跳转(倒计时跳转)代码总结
Oct 24 Javascript
Jquery创建层显示标题和内容且随鼠标移动而移动
Jan 26 Javascript
Extjs表单常见验证小结
Mar 07 Javascript
CSS3,HTML5和jQuery搜索框集锦
Dec 02 Javascript
jQuery实现响应鼠标事件的图片透明效果【附demo源码下载】
Jun 16 Javascript
终于实现了!精彩的jquery弹幕效果
Jul 18 Javascript
JavaScript中this的四个绑定规则总结
Sep 26 Javascript
浅谈js script标签中的预解析
Dec 30 Javascript
Vue2.5 结合 Element UI 之 Table 和 Pagination 组件实现分页功能
Jan 26 Javascript
React Router V4使用指南(精讲)
Sep 17 Javascript
Vue项目中最新用到的一些实用小技巧
Nov 06 Javascript
WEEX环境搭建与入门详解
Oct 16 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中文汉字验证码
2007/04/08 PHP
手把手教你打印出PDF(关于fpdf的简单应用)
2013/06/25 PHP
浅析ThinkPHP中的pathinfo模式和URL重写
2014/01/06 PHP
php支持中文字符串分割的函数
2015/05/28 PHP
PHP的简单跳转提示的实现详解
2019/03/14 PHP
laradock环境docker-compose操作详解
2019/07/29 PHP
php适配器模式简单应用示例
2019/10/23 PHP
JavaScript入门教程(11) js事件处理
2009/01/31 Javascript
向当前style sheet中插入一个新的style实现方法
2013/04/01 Javascript
jQuery截取指定长度字符串代码
2014/08/21 Javascript
Bootstrap的Carousel配合dropload.js实现移动端滑动切换图片
2017/03/10 Javascript
理解nodejs的stream和pipe机制的原理和实现
2017/08/12 NodeJs
element 结合vue 在表单验证时有值却提示错误的解决办法
2018/01/22 Javascript
vue-cli项目中使用Mockjs详解
2018/05/14 Javascript
使用p5.js实现动态GIF图片临摹重现
2019/10/23 Javascript
jquery实现拖拽添加元素功能
2020/12/01 jQuery
[02:10]三分钟回顾完美世界城市挑战赛
2019/01/24 DOTA
[49:15]DOTA2-DPC中国联赛 正赛 CDEC vs XG BO3 第二场 1月19日
2021/03/11 DOTA
python实现端口转发器的方法
2015/03/13 Python
Python中死锁的形成示例及死锁情况的防止
2016/06/14 Python
python 3.6 +pyMysql 操作mysql数据库(实例讲解)
2017/12/20 Python
使用OpenCV实现仿射变换—旋转功能
2019/08/29 Python
Python turtle库绘制菱形的3种方式小结
2019/11/23 Python
python如何查看安装了的模块
2020/06/23 Python
常用的四种CSS透明属性介绍
2014/04/12 HTML / CSS
使用HTML5在网页中嵌入音频和视频播放的基本方法
2016/02/22 HTML / CSS
HTML5 3D书本翻页动画的实现示例
2019/08/28 HTML / CSS
澳大利亚宠物商店:Petbarn
2017/11/18 全球购物
购买一个高级域名:BuyDomains
2018/03/11 全球购物
ghd法国官方网站:英国最受欢迎的美发工具品牌
2019/04/18 全球购物
新护士岗前培训制度
2014/02/02 职场文书
党员四风剖析材料
2014/08/27 职场文书
北京离婚协议书范文2014
2014/09/29 职场文书
会议主持词开场白
2015/05/28 职场文书
肖申克救赎观后感
2015/06/02 职场文书
Python 中面向接口编程
2022/05/20 Python