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
相关推荐
kyriewen118 小时前
你点的“刷新”是假刷新?前端路由的瞒天过海术
开发语言·前端·javascript·ecmascript·html5
Timer@10 小时前
LangChain 教程 04|Agent 详解:让 AI 学会“自己干活“
javascript·人工智能·langchain
阿珊和她的猫10 小时前
TypeScript中的never类型: 深入理解never类型的使用场景和特点
javascript·typescript·状态模式
skywalk816310 小时前
Kotti Next的tinyfrontend前端模仿Kotti 首页布局还是不太好看,感觉比Kotti差一点
前端
RopenYuan12 小时前
FastAPI -API Router的应用
前端·网络·python
走粥12 小时前
clsx和twMerge解决CSS类名冲突问题
前端·css
Purgatory00113 小时前
layui select重新渲染
前端·layui
weixin1997010801613 小时前
《中国供应商商品详情页前端性能优化实战》
前端·性能优化
九皇叔叔14 小时前
003-SpringSecurity-Demo 统一响应类
java·javascript·spring·springsecurity