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

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

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

相关推荐
前端拾光者7 分钟前
利用D3.js实现数据可视化的简单示例
开发语言·javascript·信息可视化
木子02041 小时前
前端VUE项目启动方式
前端·javascript·vue.js
endingCode1 小时前
45.坑王驾到第九期:Mac安装typescript后tsc命令无效的问题
javascript·macos·typescript
Myli_ing2 小时前
HTML的自动定义倒计时,这个配色存一下
前端·javascript·html
I_Am_Me_3 小时前
【JavaEE进阶】 JavaScript
开发语言·javascript·ecmascript
℘团子এ3 小时前
vue3中如何上传文件到腾讯云的桶(cosbrowser)
前端·javascript·腾讯云
学习前端的小z3 小时前
【前端】深入理解 JavaScript 逻辑运算符的优先级与短路求值机制
开发语言·前端·javascript
前端百草阁3 小时前
【TS简单上手,快速入门教程】————适合零基础
javascript·typescript
彭世瑜3 小时前
ts: TypeScript跳过检查/忽略类型检查
前端·javascript·typescript
Backstroke fish3 小时前
Token刷新机制
前端·javascript·vue.js·typescript·vue