JavaScript 封装无感 token 刷新

accessToken过期后使用refreshToken调用刷新 token 接口去获取新的accessToken

js 复制代码
// tokenManager.js
class TokenManager {
  constructor() {
    // 用于存储刷新token的promise,防止并发刷新
    this.promise = null
  }

  /**
   * 设置访问token
   * @param {string} token
   */
  setToken(token) {
    localStorage.setItem('accessToken', token)
  }

  /**
   * 获取刷新token
   * @returns {string|null}
   */
  getRefreshToken() {
    return localStorage.getItem('refreshToken')
  }

  /**
   * 移除所有token信息
   */
  removeToken() {
    localStorage.removeItem('accessToken')
    localStorage.removeItem('refreshToken')
    localStorage.removeItem('name')
  }

  /**
   * 调用接口刷新token
   * @returns {Promise<Response>}
   */
  async refreshAccessToken() {
    // 这里是刷新token的接口
    const response = await fetch('/api/v3/Logon/getRefreshUserToker', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Refresh-Token': 'true',
      },
      body: JSON.stringify({
        refreshToken: this.getRefreshToken(),
        name: localStorage.getItem('name'),
      }),
    })
    return response.json()
  }

  /**
   * 获取token,如果正在刷新则返回同一个promise
   * @returns {Promise<boolean>} 刷新成功返回true,失败返回false
   */
  refreshToken() {
    if (this.promise) return this.promise

    this.promise = new Promise(resolve => {
      this.refreshAccessToken()
        .then(res => {
          if (res.code === 0) {
            this.setToken(res.accessToken)
            resolve(true)
          } else {
            this.removeToken()
            resolve(false)
          }
        })
        .catch(err => {
          console.error('Refresh token failed', err)
          this.removeToken()
          resolve(false)
        })
    })

    this.promise.finally(() => {
      this.promise = null
    })
    return this.promise
  }
}

const tokener = new TokenManager()

export default tokener

在响应拦截中使用刷新token

js 复制代码
// request.js
import tokener from './tokenManager.js'

/**
 * 请求封装
 * @param {string} url
 * @param {RequestInit} options
 * @returns {Promise<any>}
 */
const request = async (url, options = {}) => {
  // 合并默认配置
  const config = {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
  }

  // 添加token
  const token = localStorage.getItem('accessToken')
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`
  }

  try {
    const response = await fetch(url, config)

    // 拦截 401 未授权,且不是刷新token的请求
    if (response.status === 401 && !config.headers['X-Refresh-Token']) {
      // 尝试刷新token
      const isRefreshSuccess = await tokener.refreshToken()

      if (isRefreshSuccess) {
        // 刷新成功,重试原请求
        // 更新header中的token
        const newToken = localStorage.getItem('accessToken')
        config.headers['Authorization'] = `Bearer ${newToken}`

        // 重新发起请求
        return fetch(url, config).then(res => res.json())
      } else {
        // 刷新失败,抛出错误或跳转登录
        throw new Error('Token refresh failed')
        // window.location.href = '/login'
        // 可以记录当前页面,重新登陆后跳转到当前页面
        // localStorage.setItem('redirectUrl', window.location.href)
      }
    }

    return response.json()
  } catch (error) {
    console.error('Request failed:', error)
    throw error
  }
}

export default request
相关推荐
Hilaku8 分钟前
为什么死磕 1px 的团队,用户体验反而更差?
前端·javascript·程序员
lichenyang45320 分钟前
鸿蒙元服务跳转 H5 太慢?从 Web 内核预热、网络预连接到骨架屏的完整优化方案
前端
aa小小29 分钟前
大屏的数据应该怎么做“实时更新“?大屏页面如果一直开着不关(比如挂了几天几夜),你觉得可能会出现什么问题,需要怎么预防?
前端·数据可视化
陈拉斯37 分钟前
给公司年会写了个大屏抽奖系统:动画在前端跑,凭什么说结果没被改?
javascript
aa小小1 小时前
Proxy 响应式原理
前端·proxy
计算机魔术师1 小时前
Latent Space 访谈 TypeSafe CEO Diogo Almeida:Jev 是面向生产环境的 System One 模型而非万能 God 模型
前端
yzy851 小时前
Vue中下载或打开文件的JavaScript方法
前端·javascript·vue
用户298698530141 小时前
在 React 中用 JavaScript 将 Word 转换为文本
前端·javascript·react.js
计算机魔术师2 小时前
马斯克称 Grok 4.7 使 xAI 在智能体编码领域位列第三
前端