闲说token无感刷新

无感刷新token这个问题,一直来都是面试中常见的问题,相信有不少同学也被问到过,今天就一起来讨论下这个。

概述

无感刷新是一种优化用户体验的手段。它允许应用在用户首次登录之后,通过后端服务获取一组具有时效性的认证凭据,通常是一个Token和一个RefreshToken,其中Token作为用户身份的直接证明,而RefreshToken则用于在Token过期时静默更新认证状态,从而避免了用户因为认证凭据过期而频繁重新登录这种情况。

思路

首先我们要先捋清楚整个流程的思路

主要流程大概就是:

  1. 用户发出请求
  2. 后端验证token是否失效
  3. 没有失效的话,返回正常数据(结束)
  4. 失效的话,返回对应状态码
  5. 前端判断失效,调用刷新接口更新token
  6. 重新执行第一步
graph TB a(用户发出请求) --> b{后端验证token是否失效} b --未失效--> f(正常返回) b --已失效--> e(返回已失效状态码) e --> g(调用刷新接口更新token) g --> a

具体该怎么做

基于axios

axios是目前普遍使用的基于Promise的http库,它有一个拦截器,可以在请求或者响应被thencatch处理前拦截它们。在这里,我们可以通过响应拦截拿到返回的报文,并在这里做出对Token失效的处理

举个栗子:

js 复制代码
// request.js
import { updateToken } from '@/api/index.js' // 导入刷新token方法
​
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response;
}, function (error) {
  const { status } = error.response.status // 获取到返回状态码
  if (status === 401) { // 401状态码的意思是需要身份验证(表示token已经失效)
      updateToken().then(res => { // 调用刷新token方法
          const { token, refreshToken} = res.data // 拿到更新后的token和refreshToken
          localstorage.setItem('token', {token, refreshToken}) // 更新本地存储
          axios({token}) // 使用新的token重新发送请求
      })
  } else {
      return Promise.reject(error);
  }
});

基于class封装

对于大型项目,可能需要根据不同的状态码进行多种业务处理。这种情况下,将无感刷新Token的逻辑封装在一个独立的类中,可以避免逻辑耦合,并提高代码的可维护性。

举个栗子:

js 复制代码
class RetryRequest {
    constructor({
        url,
        getRefreshToken,
        unauthorizedCode = 401,
        onSuccess,
        onError
    }) {
        this.url = url,
        this.getRefreshToken = getRefreshToken
        this.unauthorizedCode = unauthorizedCode,
        this.onSuccess = onSuccess,
        this.onError = onError
    }
    requestWrapper(request) {
        return new Promise((resolve, reject) => {
            const requestFn = request
            return request()
                .then(res => {
                    resolve(res)
                })
                .catch(err => {
                    // 表示token失效,需要更新token
                    if (err.response.status === this.unauthorizedCode) {
                        this.fetchNewToken({ // 更新token
                            headers: { Authorization: this.getRefreshToken() }
                        })
                        .then(requestFn()) // 成功则重新发起之前失败的请求
                        .catch(err => {})
                    }
                })
        })
    }
    // 获取新token的函数
    fetchNewToken(config) {
        return http.get(this.url, config)
            .then(this.onSuccess())
            .catch(this.onError())
    }
}
js 复制代码
// 使用
const getRefreshToken = () => {
    const { REFRESH_KEY } = JSON.parse(localStorage.getItem('token'))
    return REFRESH_KEY
}
const axiosRetry = new RetryRequest({
    url: '/refreshToken',
    unauthorizedCode: 401,
    getRefreshToken,
    onSuccess: () => {},
    onError: () => {}
})
​
axiosRetry.requestWrapper(() => axios.get(url, options)) // 发送请求

以上的例子只可用作参考,不可照搬,具体写法需要根据实际业务需求进行调整

场景延伸

在实际应用中,可能会遇到多个请求同时触发Token刷新的情况。为了避免重复刷新Token和性能问题,我们可以引入一个控制机制,例如使用一个全局变量来标记当前是否正在刷新Token

以上面的axios为例子:

js 复制代码
// request.js
let isRefreshing = false // 是否在刷新token
const updateToken = function() { // 重新请求token函数
    isRefreshing = true // 打开开关
    return axios.get('/refreshToken')
}
​
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
  return response;
}, function (error) {
  const { status } = error.response.status
  if (status === 401 && !isRefreshing) { // token失效且开关为关闭状态才可以执行
      updateToken().then(res => {
          const { token, refreshToken} = res.data
          localstorage.setItem('token', {token, refreshToken})
          isRefreshing = false // 关闭开关
          axios({token})
      })
  } else {
      return Promise.reject(error);
  }
});

上面这样就可以解决重复刷新token的问题了,但同时也会出现另一个问题:虽然不会重复刷新token,但因为逻辑没有往下走了,除了第一个请求外,后面那些请求不能自动重新请求了。针对这个问题,我们可以把思路稍微变一变:身份验证失败的请求我们可以存到一个数组里面,然后在更新token之后,再遍历数组执行里面的请求。

例:

js 复制代码
// request.js
let isRefreshing = false // 是否在刷新token
const requestArr = [] // 请求缓存数组
const updateToken = function() { // 重新请求token函数
    isRefreshing = true // 打开开关
    return axios.get('/refreshToken')
}
​
// 添加响应拦截器
axios.interceptors.response.use(function (response) {
  return response;
}, function (error) {
  const { status } = error.response.status
  if (status === 401) {
      requestArr.push(axios(config)) // 请求存进数组中
      if (!isRefreshing) {
          updateToken().then(res => {
              const { token, refreshToken} = res.data
              localstorage.setItem('token', {token, refreshToken})
              requestArr.forEach(cb => cb()) // 遍历执行请求
          }).finally(() => {
              requestArr = [] // 清空数组
              isRefreshing = false // 关闭开关
          })
      }
  } else {
      return Promise.reject(error);
  }
});

至此,基本的一些使用思路和场景已经说完了。还是要根据具体的业务需求和场景来判断使用与否,以及进行调整和优化,以达到最佳的性能和用户体验。

如果有什么问题的话,可以在评论区留言,大家一起探讨学习,谢谢!

相关推荐
深情废杨杨3 分钟前
前端vue-插值表达式和v-html的区别
前端·javascript·vue.js
GHUIJS3 分钟前
【vue3】vue3.3新特性真香
前端·javascript·vue.js
众生回避9 分钟前
鸿蒙ms参考
前端·javascript·vue.js
笃励1 小时前
Angular面试题五
javascript·ecmascript·angular.js
GHUIJS1 小时前
【vue3】vue3.5
前端·javascript·vue.js
-seventy-1 小时前
对 JavaScript 原型的理解
javascript·原型
秋沐2 小时前
vue中的slot插槽,彻底搞懂及使用
前端·javascript·vue.js
QGC二次开发2 小时前
Vue3 : Pinia的性质与作用
前端·javascript·vue.js·typescript·前端框架·vue
子非鱼9213 小时前
【前端】ES6:Set与Map
前端·javascript·es6
想退休的搬砖人4 小时前
vue选项式写法项目案例(购物车)
前端·javascript·vue.js