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 相关文章推荐
IE下js调试工具Companion.JS
Oct 15 Javascript
jQuery EasyUI API 中文文档 - TreeGrid 树表格使用介绍
Nov 21 Javascript
JQuery对表单元素的基本操作使用总结
Jul 18 Javascript
JavaScript中通过prototype属性共享属性和方法的技巧实例
Mar 13 Javascript
通过伪协议解决父页面与iframe页面通信的问题
Apr 05 Javascript
JavaScript实现鼠标点击后层展开效果的方法
May 13 Javascript
深入探究AngularJS框架中Scope对象的超级教程
Jan 04 Javascript
使用D3.js制作图表详解
Aug 13 Javascript
js实现登录与注册界面
Nov 01 Javascript
vue elementUI tree树形控件获取父节点ID的实例
Sep 12 Javascript
Node+OCR实现图像文字识别功能
Nov 26 Javascript
使用Typescript和ES模块发布Node模块的方法
May 25 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中遇到BOM、编码导致json_decode函数无法解析问题
2014/07/02 PHP
PHP学习笔记(二):变量详解
2015/04/17 PHP
PHP使用Redis替代文件存储Session的方法
2017/02/15 PHP
Laravel使用PHPQRCODE实现生成带有LOGO的二维码图片功能示例
2017/07/07 PHP
Yii2使用表单上传文件的实例代码
2017/08/03 PHP
PHP下 Mongodb 连接远程数据库的实例代码
2017/08/30 PHP
PHP bin2hex()函数基础实例讲解
2019/02/11 PHP
php 使用html5 XHR2实现上传文件与进度显示功能示例
2020/03/03 PHP
JQuery 插件制作实践 xMarquee插件V1.0
2010/04/02 Javascript
javascript 文件的同步加载与异步加载实现原理
2012/12/13 Javascript
js中parseInt函数浅谈
2013/07/31 Javascript
基于BootStrap的Metronic框架实现页面链接收藏夹功能按钮移动收藏记录(使用Sortable进行拖动排序)
2016/08/29 Javascript
javascript实现文字无缝滚动
2016/12/27 Javascript
JS验证全角与半角及相互转化的介绍
2017/05/18 Javascript
JavaScript使用享元模式实现文件上传优化操作示例
2018/08/07 Javascript
JS面试题大坑之隐式类型转换实例代码
2018/10/14 Javascript
微信小程序实现watch监听
2020/06/04 Javascript
[01:24:34]2014 DOTA2华西杯精英邀请赛5 24 DK VS LGD
2014/05/25 DOTA
[15:23]教你分分钟做大人:虚空假面
2014/10/30 DOTA
python实现对一个完整url进行分割的方法
2015/04/29 Python
Python实现识别图片内容的方法分析
2018/07/11 Python
详解anaconda离线安装pytorchGPU版
2020/09/08 Python
Selenium环境变量配置(火狐浏览器)及验证实现
2020/12/07 Python
HTML5获取当前地理位置并在百度地图上展示的实例
2020/07/10 HTML / CSS
英国信箱在线鲜花速递公司:Bloom & Wild
2019/03/10 全球购物
印尼在线购买隐形眼镜网站:Lensza.co.id
2019/04/27 全球购物
苏格兰领先的多渠道鞋店:Begg Shoes
2019/10/22 全球购物
物流管理毕业生自荐信范文
2014/03/15 职场文书
经贸专业毕业生求职信范文
2014/05/01 职场文书
中学生民族团结演讲稿
2014/08/27 职场文书
乡镇领导班子四风对照检查材料
2014/09/27 职场文书
教师作风整改措施思想汇报
2014/10/12 职场文书
会议简讯范文
2015/07/20 职场文书
新郎父亲婚礼致辞
2015/07/27 职场文书
学术会议开幕词
2016/03/03 职场文书
MySQL中InnoDB存储引擎的锁的基本使用教程
2021/05/26 MySQL