最近遇到个需求:前端登录后,后端返回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 = 'https://www.jb51.net/' }) } return response }, error => { return Promise.reject(error) }) function refreshToken () { // instance是当前request.js中已创建的axios实例 return instance.post('/refreshtoken').then(res => res.data) }