前端实现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);
  }
);
相关推荐
QQ1__8115175154 小时前
Spring boot名城小区物业管理系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】
前端·vue.js·spring boot
钛态4 小时前
前端微前端架构:大项目的救命稻草还是自找麻烦?
前端·vue·react·web
一粒黑子5 小时前
【实战解析】阿里开源 PageAgent:纯前端 GUI Agent,一行JS让网页支持自然语言操控
前端·javascript·开源
独角鲸网络安全实验室5 小时前
2026微信小程序抓包全解析:从实操落地到合规风控,解锁前端调试新范式
前端·微信小程序·小程序·抓包·系统代理绕过·https证书严格校验·进程隔离
紫微AI5 小时前
前端文本测量成了卡死一切创新的最后瓶颈,pretext实现突破了
前端·人工智能·typescript
GISer_Jing5 小时前
AI前端(From豆包)
前端·aigc·ai编程
IT枫斗者5 小时前
前端部署后如何判断“页面是不是最新”?一套可落地的版本检测方案(适配 Vite/Vue/React/任意 SPA)
前端·javascript·vue.js·react.js·架构·bug
测试修炼手册5 小时前
[测试技术] 深入理解 JSON Web Token (JWT)
前端·json
AI老李5 小时前
2026 年 Web 前端开发的 8 个趋势!
前端
里欧跑得慢5 小时前
15. Web可访问性最佳实践:让每个用户都能平等访问
前端·css·flutter·web