Token 刷新方案

方案一,双 token

accessToken 和 refreshToken,accessToken 用于请求获取业务数据,refreshToken 用于请求获取 accessToken。

无感刷新

请求肯定有先有后,即使同时发起,说白了就是当一个请求返回 401 时,立即保存其他也返回 401 的请求,并且去拿 refreshToken 请求获取新的 accessToken,然后把刚才保存的 401 请求重新再跑一遍,这样就不会出现一个请求正常返回数据,其他请求错误,操作再次发起请求,又全都可以的情况了。

import axios from 'axios'
import router from '@/router'

const service = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 15000
})

// ==================== 全局状态定义(通俗易懂)====================
// 刷新锁是否开启:false=锁关闭(空闲)  true=锁开启(正在刷新)
let refreshLockOpen = false
// 请求等待队列:存刷新期间失败的接口请求
let waitRequestQueue = []

// ==================== Token 工具方法 ====================
// 获取短期授权令牌
export const getAccessToken = () => localStorage.getItem('accessToken')
// 获取长期刷新令牌
export const getRefreshToken = () => localStorage.getItem('refreshToken')

// 保存双令牌
export const saveTwoToken = (accessToken, refreshToken) => {
  localStorage.setItem('accessToken', accessToken)
  localStorage.setItem('refreshToken', refreshToken)
}

// 清空令牌 退出登录
export const logoutClearToken = () => {
  localStorage.removeItem('accessToken')
  localStorage.removeItem('refreshToken')
  waitRequestQueue = []
  router.replace('/login')
}

// ==================== 刷新Token接口 ====================
const getNewToken = () => {
  return axios.post(`${import.meta.env.VITE_API_BASE_URL}/user/refresh-token`, {
    refreshToken: getRefreshToken()
  })
}

// ==================== 请求拦截器 ====================
service.interceptors.request.use(
  config => {
    const token = getAccessToken()
    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }
    return config
  },
  err => Promise.reject(err)
)

// ==================== 响应拦截器 核心无感刷新 ====================
service.interceptors.response.use(
  res => res.data,
  async error => {
    const { response, config } = error
    if (!response) return Promise.reject(error)

    // 只处理401令牌过期
    if (response.status !== 401) {
      return Promise.reject(error)
    }

    // 没有刷新令牌,直接下线
    if (!getRefreshToken()) {
      logoutClearToken()
      return Promise.reject(error)
    }

    // ========== 核心锁逻辑 ==========
    // 判断:刷新锁 没有开启(当前空闲)
    if (!refreshLockOpen) {
      // 立刻【开启刷新锁】,禁止其他请求再刷新
      refreshLockOpen = true

      try {
        // 调用接口获取新令牌
        const result = await getNewToken()
        const { accessToken, refreshToken } = result.data
        // 保存新令牌
        saveTwoToken(accessToken, refreshToken)

        // 批量放行队列里所有等待的请求
        waitRequestQueue.forEach(cb => cb(accessToken))
        // 清空队列
        waitRequestQueue = []

        // 重试当前失败的接口
        config.headers.Authorization = `Bearer ${accessToken}`
        return service(config)
      } catch (refreshErr) {
        // 刷新令牌失效,强制退出登录
        logoutClearToken()
        return Promise.reject(refreshErr)
      } finally {
        // 无论成功失败,最后【关闭刷新锁】恢复空闲状态
        refreshLockOpen = false
      }
    } 
    // 刷新锁已经开启(正在刷新中)
    else {
      // 把当前失败请求加入队列排队等待
      return new Promise(resolve => {
        waitRequestQueue.push((newAccessToken) => {
          config.headers.Authorization = `Bearer ${newAccessToken}`
          resolve(service(config))
        })
      })
    }
  }
)

export default service

提前刷新

如果希望做的更完善,还可以提前刷新。在登录接口和 refresh 接口返回 refreshToken 的同时,还返回一个 refreshToken 过期时间戳,建议是放在 refreshToken 载体 JWT 里,前端自行解析后保存,后续可以在请求拦截器里判断一下时间戳是否过期,如果快过期了就请求 refresh 接口获取一个新的 accessToken。

方案二,单 token

如果是唯一 token 的情况,就要么是首次登录时,不仅返回一个 token,还返回这个 token 的过期时间,每次发请求时或者设置个定时器,检查快到 token 过期时间时,拿旧 token 去请求刷新 token 接口,获取新 token。 主旨就是从各种渠道检查是否过期,不管是前端定时器检查、请求时时间差计算检查、后端定时检查并在接口中返回告知,都可以。

场景

场景痛点

有些业务流程比较长,需要填写较多表单,当停滞在某个环节时间较长,期间未发送过任何 token,一旦超过 token 有效时长而不自知继续流程填写表单,最终提交时 100% token 失效,提交失败,填写数据丢失。

解决方案

对 mousedown,keydown,mousemove 等操作行为进行事件监听,并记录下当下时间作为最后操作时间,可以根据实际情况做防抖。

然后可以隔几秒检查一下当前时间和最后操作时间的时间差,如果超过 token 有效时长,就弹窗提示。 或者把检查逻辑融合到事件监听中,同样是检查最后操作时间和上次操作时间。


已发布

分类

来自

标签:

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注