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

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

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

相关推荐
我命由我1234527 分钟前
React Router 6 - 编程式路由导航、useInRouterContext、useNavigationType
前端·javascript·react.js·前端框架·html·ecmascript·js
橙露2 小时前
JavaScript 异步编程:Promise、async/await 从原理到实战
开发语言·javascript·ecmascript
我命由我123452 小时前
React Router 6 - 嵌套路由、路由传递参数
前端·javascript·react.js·前端框架·html·ecmascript·js
十六年开源服务商3 小时前
2026年WordPress网站地图完整指南
java·前端·javascript
英俊潇洒美少年3 小时前
MessageChannel 如何实现时间切片
javascript·react.js·ecmascript
技术钱5 小时前
react数据大屏四种适配方案
javascript·react.js·ecmascript
李明卫杭州5 小时前
JavaScript 严格模式下 arguments 的区别
前端·javascript
一次旅行5 小时前
今日心理学知识分享(三)
开发语言·javascript·程序人生·ecmascript
牛十二6 小时前
openclaw安装mcporter搜索小红书
开发语言·javascript·ecmascript
小金鱼Y6 小时前
🔥 前端人必看:浏览器安全核心知识点全解析(XSS/CSRF/DDoS)
前端·javascript·安全