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 相关文章推荐
jQuery.extend()的实现方式详解及实例
Jun 29 Javascript
用jquery.sortElements实现table排序
May 04 Javascript
在jQuery中使用$而避免跟其它库产生冲突的方法
Aug 13 Javascript
jquery实现的蓝色二级导航条效果代码
Aug 24 Javascript
全面解析Bootstrap表单使用方法(表单控件状态)
Nov 24 Javascript
node.js中module.exports与exports用法上的区别
Sep 02 Javascript
AngularJs上传前预览图片的实例代码
Jan 20 Javascript
微信小程序template模板实例详解
Oct 27 Javascript
React教程之封装一个Portal可复用组件的方法
Jan 02 Javascript
JavaScript switch语句使用方法简介
Dec 30 Javascript
vue2路由基本用法实例分析
Mar 06 Javascript
解决vue elementUI 使用el-select 时 change事件的触发问题
Nov 17 Vue.js
解决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
Joomla简单判断用户是否登录的方法
2016/05/04 PHP
php微信公众号开发(2)百度BAE搭建和数据库使用
2016/12/15 PHP
php实现数组纵向转横向并过滤重复值的方法分析
2017/05/29 PHP
PHP PDOStatement::bindColumn讲解
2019/01/30 PHP
jquery一般方法介绍 入门参考
2011/06/21 Javascript
基于jquery可配置循环左右滚动例子
2011/09/09 Javascript
js中自定义方法实现停留几秒sleep
2014/07/11 Javascript
js禁止页面刷新与后退的方法
2015/06/08 Javascript
下雪了 javascript实现雪花飞舞
2020/08/02 Javascript
将List对象列表转换成JSON格式的类实现方法
2016/07/04 Javascript
微信小程序 video组件详解
2016/10/25 Javascript
BootStrap便签页的简单应用
2017/01/06 Javascript
最全的JavaScript开发工具列表 总有一款适合你
2017/06/29 Javascript
使用Node搭建reactSSR服务端渲染架构
2018/08/30 Javascript
AngularJs1.x自定义指令独立作用域的函数传入参数方法
2018/10/09 Javascript
layui radio点击事件实现input显示和隐藏的例子
2019/09/02 Javascript
javascript实现点击小图显示大图
2020/11/29 Javascript
[01:09:01]完美世界DOTA2联赛循环赛 Magma vs PXG BO2第一场 10.28
2020/10/28 DOTA
[01:06:18]DOTA2-DPC中国联赛 正赛 Phoenix vs Dynasty BO3 第二场 1月26日
2021/03/11 DOTA
[01:20:47]DOTA2-DPC中国联赛 正赛 Ehome vs Magma BO3 第一场 1月19日
2021/03/11 DOTA
对Python捕获控制台输出流的方法详解
2019/01/07 Python
使用Python检测文章抄袭及去重算法原理解析
2019/06/14 Python
python 类的继承 实例方法.静态方法.类方法的代码解析
2019/08/23 Python
python文字转语音实现过程解析
2019/11/12 Python
python利用paramiko实现交换机巡检的示例
2020/09/22 Python
Python识别验证码的实现示例
2020/09/30 Python
Python实现Word文档转换Markdown的示例
2020/12/22 Python
找到不普通的东西:Bonanza
2016/10/20 全球购物
巴西食品补充剂在线零售商:Músculos na Web
2017/08/07 全球购物
统计学专业毕业生的自我评价分享
2013/11/28 职场文书
道路交通安全实施方案
2014/03/12 职场文书
陈安之励志演讲稿
2014/08/21 职场文书
创业者如何撰写出一份打动投资人的商业计划书?
2019/07/02 职场文书
读《解忧杂货店》有感:请相信一切都是最好的安排
2019/11/07 职场文书
《分一些蚊子进来》读后感3篇
2020/01/09 职场文书
零基础学java之带返回值的方法的定义和调用
2022/04/10 Java/Android