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
相关推荐
子兮曰5 小时前
async/await高级模式:async迭代器、错误边界与并发控制
前端·javascript·github
恋猫de小郭5 小时前
2026 Flutter VS React Native ,同时在 AI 时代 VS Native 开发,你没见过的版本
android·前端·flutter
GIS之路7 小时前
ArcGIS Pro 中的 Notebooks 入门
前端
IT_陈寒9 小时前
React状态管理终极对决:Redux vs Context API谁更胜一筹?
前端·人工智能·后端
Kagol10 小时前
TinyVue 支持 Skills 啦!现在你可以让 AI 使用 TinyVue 组件搭建项目
前端·agent·ai编程
柳杉10 小时前
从零打造 AI 全球趋势监测大屏
前端·javascript·aigc
simple_lau10 小时前
Cursor配置MasterGo MCP:一键读取设计稿生成高还原度前端代码
前端·javascript·vue.js
睡不着先生10 小时前
如何设计一个真正可扩展的表单生成器?
前端·javascript·vue.js
天蓝色的鱼鱼10 小时前
模块化与组件化:90%的前端开发者都没搞懂的本质区别
前端·架构·代码规范
明君8799710 小时前
Flutter 如何给图片添加多行文字水印
前端·flutter