前端实现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);
  }
);
相关推荐
天渺工作室9 分钟前
实现一个adblock/adblock plus等浏览器广告拦截器检测插件
前端·javascript
阳光是sunny33 分钟前
Vue 项目怎么做用户行为全链路监控?轻量插件方案详解
前端·面试·架构
ZhengEnCi1 小时前
Q04-Vite禁用CSS代码分割-解决生产环境样式加载顺序混乱问题
前端·vue.js·vite
九酒1 小时前
AI Agent 开发踩坑记:口播功能非得用 APP 原生实现吗?
前端·人工智能·agent
Jackson__2 小时前
做了一段时间的AI coding后,我终于搞清了 CLI 和 MCP 的区别
前端·agent·ai编程
IT_陈寒4 小时前
JavaScript项目实战经验分享
前端·人工智能·后端
用户47949283569155 小时前
6w star,GitHub 趋势第一的 Ponytail,这个agent插件到底在火什么
前端·后端
薛定喵的谔6 小时前
我开源了一个精致的 Next.js 博客模板:Skyplume
前端·前端框架·next.js
张龙6877 小时前
构建生产级 AI Agent:工具调用与记忆架构实战指南
前端
kyriewen8 小时前
2026 年了,还在用 Node.js?Bun 迁移实战:20 分钟搞定,附踩坑记录
前端·javascript·node.js