前端实现token无感刷新

1.在common.ts定义刷新token的接口

javascript 复制代码
export function getAuthToken(data?: Object) {
	return request({
		url: '/admin/oauth2/retoken',
		method: 'post',
		data,
	});
}

2.创建token.ts文件

javascript 复制代码
import Cookies from 'js-cookie';
import { getAuthToken } from '/@/api/common'
import { ElMessage } from 'element-plus';

let timer;

export interface TokenData {
  accessToken: string;
  refreshToken: string;
  expiresIn?: number;
}

class TokenManager {
  private accessToken: string | null = null;
  private refreshToken: string | null = null;
  private refreshPromise: Promise<TokenData> | null = null;

  constructor() {
    this.loadTokens();
  }

  // 从 Cookies 加载 tokens
  private loadTokens() {
    this.accessToken = Cookies.get('access_token');
    this.refreshToken = Cookies.get('refresh_token');
  }

  // 设置 tokens
  setTokens(tokens: TokenData): void {
    this.accessToken = tokens.accessToken;
    this.refreshToken = tokens.refreshToken;
    
    Cookies.set('access_token', tokens.accessToken);
    Cookies.set('refresh_token', tokens.refreshToken);
    
    if (tokens.expiresIn) {
      const expireTime = Date.now() / 1000 + tokens.expiresIn;
      Cookies.set('tokenAgeing', expireTime.toString());
    }
  }

  // 获取 access token
  getAccessToken(): string | null {
    return Cookies.get('access_token');
  }

  // 获取 refresh token
  getRefreshToken(): string | null {
    return Cookies.get('refresh_token');
  }

  // 清除 Promise
  clearPromise(): void {
    this.refreshPromise = null;
  }

  // 检查 token 是否即将过期
  isTokenExpiring(): boolean {
    const expireTime = Cookies.get('tokenAgeing');
    if (!expireTime) return false;
    
    const currentTime = Date.now();
    const timeUntilExpire = parseInt(expireTime) * 1000 - currentTime;
    
    // 提前 5 分钟刷新
    return timeUntilExpire < 5 * 60 * 1000;
  }

  // 刷新 token
  async refreshTokens(): Promise<TokenData> {
    // 如果已经在刷新,返回同一个 promise
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    const refreshToken = this.getRefreshToken();
    if (!refreshToken) {
      ElMessage.error('refreshToken不存在!');
      timer = setTimeout(()=>{
        console.log('跳转到登录页')
      }, 2000)
    }

    this.refreshPromise = new Promise(async (resolve, reject) => {
      try {
        getAuthToken({ refreshToken: refreshToken }).then((res) => {
          console.log(res)
          if(res.code === 200 && res.data !== null){
            this.setTokens(res.data?.list[0]);
            resolve(res.data?.list[0]);
          }else{
            ElMessage.error('token刷新失败!');
            timer = setTimeout(()=>{
              console.log('跳转到登录页')
            }, 2000)
          }
        });
      } catch (error) {
        console.log(error)
        reject(error);
      } finally {
        this.clearPromise()
      }
    });

    return this.refreshPromise;
  }
}

export const tokenManager = new TokenManager();

3.在request.ts中进行接口拦截处理

javascript 复制代码
import axios from 'axios';
import { tokenManager } from '/@/utils/token';
import { ElMessage } from 'element-plus';

let timer;

// 使用 accessToken(短期)和 refreshToken(长期)

// 请求拦截器
axios.interceptors.request.use(config => {
  // 统一增加Authorization请求头
  const token = tokenManager.getAccessToken();
  config.headers.Authorization = `Bearer ${token}`;

  // 如果 token 即将过期,提前刷新
  if (tokenManager.isTokenExpiring() && !config.url?.includes('/admin/oauth2/retoken')) {
      try {
        await tokenManager.refreshTokens();
        const newToken = tokenManager.getAccessToken();
        if (newToken) {
          config.headers.Authorization = `Bearer ${newToken}`;
        }
      } catch (error) {
        // 刷新失败
        tokenManager.clearTokens();

        ElMessage.error('token刷新失败!');
        timer = setTimeout(()=>{
          console.log('跳转到登录页')
        }, 2000)
      }
    }
  return config;
});

// 创建请求唯一标识符
const createRequestId = (config): string => {
  return `${config.method}-${config.url}-${JSON.stringify(config.data)}`;
};

// 在全局状态中管理重试记录
const retryRecords = new Map<string, number>();

// 响应拦截器处理函数
const handleResponse = (response) => {
  // 请求成功
  const requestId = createRequestId(response.config);
  retryRecords.delete(requestId);//清除重试记录
  tokenManager.clearPromise() //清除promise
	return response.data;
};

// 响应拦截器
axios.interceptors.response.use(
  response => handleResponse(response ),
  async error => {
    const originalRequest = error.config;
    const requestId = createRequestId(originalRequest);
    
    // 检查重试次数
    const retryCount = retryRecords.get(requestId) || 0;
    // 如果响应状态码为401,且请求的URL不是刷新token的接口
    if (error.response?.status === 401 && !originalRequest.url?.includes('/admin/oauth2/retoken') && retryCount < 1) {
      retryRecords.set(requestId, retryCount + 1);
      
      try {
        // 刷新 token
        await tokenManager.refreshTokens();
        const newToken = tokenManager.getAccessToken();
        if (newToken) {
          // 更新 Authorization 头
          config.headers.Authorization = `Bearer ${newToken}`
          // 重试原始请求
          return axios(originalRequest);
        }
      } catch (refreshError) {
        retryRecords.delete(requestId);
        // 刷新失败
        tokenManager.clearTokens();

        ElMessage.error('token刷新失败!');
        timer = setTimeout(()=>{
          console.log('跳转到登录页')
        }, 2000)
        return Promise.reject(refreshError);
      }
    }
    
    retryRecords.delete(requestId);
    return Promise.reject(error);
  }
);
相关推荐
寅时码4 小时前
React 之死·终章:一个 useRef,把闭包陷阱、依赖数组、漫天 rerender 全送走
前端·react.js·ai编程
不好听6134 小时前
HTML 事件监听机制:从 DOM Level 0 到 React 合成事件
前端·react.js·html
wordbaby4 小时前
为什么刷新页面就 404?聊聊 SPA 路由与 Nginx 的那点事
前端
daols884 小时前
vxe-table 渲染器教程一:实现金额输入控件
javascript·vue.js·vxe-table
不好听6134 小时前
React 核心概念:JSX、组件与数据驱动
前端·react.js
触底反弹4 小时前
🔥 React 零基础入门(中):500 行屎山代码到组件化的蜕变
前端·javascript·react.js
不好听6134 小时前
React vs Vue:两大前端框架技术选型深度对比
前端·vue.js·react.js
GuWenyue4 小时前
Cursor黑盒拆解!1套LangChain.js手写Mini编程Agent,自动生成React项目,效率提升60%
前端·数据库·人工智能
GuWenyue4 小时前
传统Agent工具两大痛点!300行代码落地MCP跨语言工具,彻底解耦LLM与工具
前端·人工智能·算法
小林ixn5 小时前
从 onclick 到 React 合成事件,再到完整应用逻辑:一次前端架构的深度剖析
前端·react.js·前端框架