闲说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.022 分钟前
onenet接口
开发语言·前端·javascript·嵌入式硬件
h***839330 分钟前
JavaScript开源
开发语言·javascript·ecmascript
Z***258043 分钟前
JavaScript虚拟现实案例
开发语言·javascript·vr
safestar20122 小时前
React 19 深度解析:从并发模式到数据获取的架构革命
前端·javascript·react.js
Wild~~2 小时前
electron-vite
前端·javascript·electron
by__csdn2 小时前
Electron+Vite:实现electron + vue3 + ts + pinia + vite高效跨平台开发指南
前端·javascript·vue.js·typescript·electron·node.js·vue
国服第二切图仔2 小时前
Electron 鸿蒙pc开发环境搭建完整保姆级教程(window)
javascript·electron·harmonyos
宋辰月3 小时前
zustand
前端·javascript·html
z***I3943 小时前
JavaScript原型链
开发语言·前端·javascript
x***58703 小时前
JavaScript语音识别开发
开发语言·javascript·语音识别