axios jwt token无感刷新

简介

jwt有两个token,分别是access_token、refresh_token;通常refresh_token设置较长的有效时间,access_token设置较短的有效时间。access_token用于后端验证,refresh_token用于获取新的access_token。也就是说,只要refresh_token不过期就可以一直保持登陆状态。 使用localStorage存储refresh_token在页面关闭后重新访问也能保持称登录状态。

代码实现

代码实现:

使用axios的拦截器,对于http响应状态401的进行拦截,加入待重发列表,刷新token,刷新成功,重发失败的请求,否则登录失败,跳转到登录页面。 另外如果请求较为密集,请求失败的比较集中,也可以在请求拦截器中作拦截。

js 复制代码
import axios from 'axios'
import router from '@/router'
const baseURL = 'http://127.0.0.1:8100/'

export const getServerUrl = () => {
  return baseURL
}

function refreshToken() {
  let instance = axios.create()
  return instance({
    baseURL: baseURL,
    url: '/user/refresh/',
    method: 'post',
    data: {
      refresh: window.localStorage.getItem('refresh_token'),
    },
  })
}

const instance = axios.create({
  baseURL: baseURL,
})
let isRefreshing = false // 是否正在刷新
let requests = [] // 存储当前token失效时正在请求的请求

function processQueue(error, token = null) {
  requests.forEach((promise) => {
    if (error) {
      promise.reject(error)
    } else {
      promise.resolve(token)
    }
  })
  requests = []
}

instance.interceptors.request.use(
  function (config) {
    console.log('当前在发送请求')
    let access_token = window.localStorage.getItem('access_token')
    config.headers.Authorization = 'Bearer ' + access_token
    return config
  },
  function (error) {
    return Promise.reject(error)
  },
)

// 添加响应拦截器
instance.interceptors.response.use(
  function (response) {
    if (response.status || String(response.status).indexOf('2') === 0) {
      return response.data
    }
  },
  function (error) {
    // 处理token失效,实现无感刷新
    const { config, response } = error
    if (response && response.status === 401) {
      console.log('token失效,开始刷新token')
      if (!isRefreshing) {
        isRefreshing = true
        // 发送刷新token的请求并返回
        return refreshToken()
          .then((res) => {
            window.localStorage.setItem('access_token', res.data.access)
            // 对requests列表中存储的请求重新执行
            processQueue(null, res.data.access)
            requests = []
            //对当前失败的附上新的token,重新发起
            config.headers.Authorization = 'Bearer ' + res.data.access
            return instance(config)
          })
          .catch((error) => {
            console.log('refreshToken failed,', error)
            window.localStorage.removeItem('access_token')
            window.localStorage.removeItem('refresh_token')
            processQueue(error) // 刷新token失败,执行所有请求的回调函数
            router.replace({ path: '/login' })
            return Promise.reject(error)
          })
          .finally(() => {
            isRefreshing = false
          })
      } else {
        // 已经有其他的请求失败后去刷新了
        return new Promise((resolve) => {
          requests.push((token) => {
            config.headers.Authorization = 'Bearer ' + token
            resolve(instance(config))
          })
        })
      }
    }
    if (response && response.status === 404) {
      console.log('请求404')
      return Promise.reject('错误!!')
    }
    // 超出 2xx 范围的状态码都会触发该函数。
    return Promise.reject(error)
  },
)

export default instance
参考文章

转载 segmentfault.com/a/119000002...

需求

最近遇到个需求:前端登录后,后端返回tokentoken有效时间,当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基本骨架:

javascript 复制代码
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拦截实现

后端接口一般会有一个约定好的数据结构,如:

yaml 复制代码
{code: 1234, message: 'token过期', data: {}}

如我这里,后端约定当code === 1234时表示token过期了,此时就要求刷新token

javascript 复制代码
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的接口

javascript 复制代码
// 是否正在刷新的标记
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,逐个重试。最终代码:

javascript 复制代码
// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let 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))
        // 重试完了别忘了清空这个队列(掘金评论区同学指点)
        requests = []
        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)
})
相关推荐
answerball5 天前
告别“原地踏步”!Vue Router:带你飞跃单页应用的迷宫 🧭 (附 Vite 快速搭建教程)
前端·vue.js·vue-router
她和夏天一样热5 天前
【前端系列】优化axios响应拦截器
java·前端·axios
她和夏天一样热5 天前
【前端系列】Pinia状态管理库
前端·axios·pinia
无奈何杨9 天前
Vben5登录过期无法再次登录问题,http状态码
前端·http·vue3·axios
放逐者-保持本心,方可放逐14 天前
SSE 流式场景应用 及 方案总结
javascript·axios·fetch·eventsource
阿髙18 天前
ios的safari下载文件 文件名乱码
前端·axios·safari·下载
一雨方知深秋20 天前
智慧商城:搜索页面基于商品名称进行搜索,分类页面点击商品进行搜索。(跳转到商品列表页面,地址栏携带的参数作为请求的参数进行请求,然后动态渲染)
前端·javascript·vue.js·axios·get·query·params
一雨方知深秋24 天前
购物车案例--分模块存储数据,发送请求数据渲染,底部总计数量和价格
前端·javascript·vue.js·axios·vuex·actions·mutations
啊·贤1 个月前
初级报错:循环引用
前端·javascript·vue3·axios