闲说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);
  }
});

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

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

相关推荐
长风清留扬1 分钟前
小程序毕业设计-音乐播放器+源码(可播放)下载即用
javascript·小程序·毕业设计·课程设计·毕设·音乐播放器
m0_7482478015 分钟前
Flutter Intl包使用指南:实现国际化和本地化
前端·javascript·flutter
ZJ_.42 分钟前
WPSJS:让 WPS 办公与 JavaScript 完美联动
开发语言·前端·javascript·vscode·ecmascript·wps
joan_851 小时前
layui表格templet图片渲染--模板字符串和字符串拼接
前端·javascript·layui
还是大剑师兰特2 小时前
什么是尾调用,使用尾调用有什么好处?
javascript·大剑师·尾调用
Watermelo6172 小时前
详解js柯里化原理及用法,探究柯里化在Redux Selector 的场景模拟、构建复杂的数据流管道、优化深度嵌套函数中的精妙应用
开发语言·前端·javascript·算法·数据挖掘·数据分析·ecmascript
一个处女座的程序猿O(∩_∩)O4 小时前
小型 Vue 项目,该不该用 Pinia 、Vuex呢?
前端·javascript·vue.js
燃先生._.10 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖11 小时前
[react]searchParams转普通对象
开发语言·前端·javascript