You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在JavaScript客户端实现多Access Token过期自动刷新功能?

多Access Token自动刷新的实现思路与方案

我之前刚好做过多Token场景下的自动刷新功能,踩过一些坑,分享下我的经验和可行方案,应该能解决你的困惑。

首先说下为什么直接用多个setTimeout会麻烦:每个Token对应一个定时器,你得手动跟踪所有定时器ID,更新Token时还要记得清除旧的定时器,很容易漏处理导致重复刷新或者失效的定时器残留。而你提到的订阅模式其实可行,但对这个场景来说,有更简洁的两种方案可以选择:


方案一:全局轮询+统一Token状态管理

这个方案适合Token数量较多的情况,不用给每个Token单独设定时器,而是定期检查所有Token的过期时间,判断是否需要刷新。

核心实现思路:

  1. 用一个TokenManager类统一存储所有Token的信息(包括accessToken、refreshToken、过期时间、受众标识audience),还要加一个isRefreshing标记防止同一Token重复触发刷新请求。
  2. 启动一个全局的定时任务(比如每隔10秒),遍历所有Token,计算距离过期还有多久,提前30秒触发刷新(避免刚好在过期时发送请求失败)。
  3. 刷新成功后更新Token信息,失败则标记错误并可以设置重试机制。

代码示例:

class TokenManager {
  constructor() {
    // 用Map存储Token,key用audience区分不同Token
    this.tokens = new Map();
    // 全局检查定时器
    this.checkInterval = null;
    // 提前刷新的缓冲时间(秒),建议设30秒避免过期临界点请求失败
    this.refreshLeadTime = 30;
  }

  // 添加/更新Token
  addToken(audience, accessToken, refreshToken, expiresAtInEpochSeconds) {
    this.tokens.set(audience, {
      accessToken,
      refreshToken,
      expiresAt: expiresAtInEpochSeconds,
      isRefreshing: false
    });

    // 如果还没启动全局检查,立即启动
    if (!this.checkInterval) {
      this.startCheckTimer();
    }

    // 可选:把Token持久化到localStorage,页面刷新后恢复
    this.persistTokens();
  }

  // 启动全局定时检查
  startCheckTimer() {
    // 每隔10秒检查一次Token状态
    this.checkInterval = setInterval(() => {
      this.checkAllTokens();
    }, 10 * 1000);
  }

  // 遍历检查所有Token
  async checkAllTokens() {
    const now = Date.now() / 1000;
    for (const [audience, tokenData] of this.tokens.entries()) {
      // 计算距离需要刷新的剩余时间
      const timeUntilRefresh = tokenData.expiresAt - now - this.refreshLeadTime;
      
      // 满足刷新条件且未在刷新中
      if (timeUntilRefresh <= 0 && !tokenData.isRefreshing) {
        tokenData.isRefreshing = true;
        try {
          // 调用你的刷新接口,获取新Token
          const newTokenInfo = await this.refreshTokens(audience, tokenData.refreshToken);
          // 更新Token信息
          this.addToken(audience, newTokenInfo.accessToken, newTokenInfo.refreshToken, newTokenInfo.expiresAt);
        } catch (error) {
          console.error(`刷新${audience}的Token失败:`, error);
          // 刷新失败,解除刷新标记,可添加重试逻辑
          tokenData.isRefreshing = false;
          // 示例:5分钟后重试
          setTimeout(() => {
            this.checkAllTokens();
          }, 5 * 60 * 1000);
        }
      }
    }
  }

  // 实际的Token刷新接口,需要你根据后端实现调整
  async refreshTokens(audience, refreshToken) {
    const response = await fetch(`/api/auth/refresh`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ audience, refreshToken })
    });
    if (!response.ok) throw new Error('刷新Token请求失败');
    return response.json();
  }

  // 获取可用的AccessToken
  getValidToken(audience) {
    const tokenData = this.tokens.get(audience);
    if (!tokenData) throw new Error(`未找到${audience}对应的Token`);
    
    const now = Date.now() / 1000;
    if (tokenData.expiresAt <= now) throw new Error(`${audience}的Token已过期`);
    return tokenData.accessToken;
  }

  // 持久化Token到localStorage
  persistTokens() {
    const tokenObj = Object.fromEntries(this.tokens.entries());
    localStorage.setItem('app_tokens', JSON.stringify(tokenObj));
  }

  // 从localStorage恢复Token
  restoreTokens() {
    const savedTokens = JSON.parse(localStorage.getItem('app_tokens') || '{}');
    for (const [audience, data] of Object.entries(savedTokens)) {
      this.addToken(audience, data.accessToken, data.refreshToken, data.expiresAt);
    }
  }

  // 销毁管理器,清除定时器
  destroy() {
    if (this.checkInterval) clearInterval(this.checkInterval);
    this.tokens.clear();
    localStorage.removeItem('app_tokens');
  }
}

// 使用示例
const tokenManager = new TokenManager();
// 页面加载时恢复Token
tokenManager.restoreTokens();
// 添加新Token
tokenManager.addToken('user_api', 'xxxxxx', 'yyyyyy', 1720000000);

方案二:单个Token对应setTimeout+统一管理定时器

如果你的Token数量不多,这个方案更精准,不需要轮询,每个Token设置自己的刷新定时器,同时通过管理器统一跟踪定时器ID,更新Token时清除旧定时器。

核心实现思路:

  1. 每个Token添加时,计算刷新时间,设置setTimeout,并把定时器ID存在Token信息里。
  2. 当Token更新(比如刷新成功),先清除旧的定时器,再设置新的定时器。
  3. 同样加入isRefreshing标记防止重复请求。

代码示例(关键部分):

class TokenManager {
  constructor() {
    this.tokens = new Map();
    this.refreshLeadTime = 30;
  }

  addToken(audience, accessToken, refreshToken, expiresAtInEpochSeconds) {
    // 清除旧的定时器(如果存在)
    const oldTokenData = this.tokens.get(audience);
    if (oldTokenData?.timeoutId) {
      clearTimeout(oldTokenData.timeoutId);
    }

    const now = Date.now() / 1000;
    const timeUntilRefresh = expiresAtInEpochSeconds - now - this.refreshLeadTime;
    let timeoutId = null;

    if (timeUntilRefresh > 0) {
      timeoutId = setTimeout(async () => {
        const tokenData = this.tokens.get(audience);
        if (!tokenData || tokenData.isRefreshing) return;
        
        tokenData.isRefreshing = true;
        try {
          const newTokenInfo = await this.refreshTokens(audience, tokenData.refreshToken);
          this.addToken(audience, newTokenInfo.accessToken, newTokenInfo.refreshToken, newTokenInfo.expiresAt);
        } catch (error) {
          console.error(`刷新${audience}Token失败:`, error);
          tokenData.isRefreshing = false;
          // 重试:1分钟后再次尝试设置定时器
          setTimeout(() => {
            this.addToken(audience, accessToken, refreshToken, expiresAtInEpochSeconds);
          }, 60 * 1000);
        }
      }, timeUntilRefresh * 1000);
    }

    this.tokens.set(audience, {
      accessToken,
      refreshToken,
      expiresAt: expiresAtInEpochSeconds,
      timeoutId,
      isRefreshing: false
    });

    this.persistTokens();
  }

  // 其他方法(refreshTokens、getValidToken等)和方案一一致
}

关键注意点

  1. 避免并发刷新:一定要加isRefreshing标记,防止同一Token被多个定时器触发多次刷新请求。
  2. 刷新失败处理:网络波动或者Refresh Token过期都会导致刷新失败,需要添加重试机制,或者在多次失败后提示用户重新登录。
  3. Token持久化:页面刷新后Token会丢失,所以要把Token存在localStorage或sessionStorage,加载时恢复。
  4. 提前刷新缓冲:不要等到Token过期再刷新,提前30-60秒刷新,确保新Token在旧Token过期前已经可用。

内容的提问来源于stack exchange,提问作者Juanma Perez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.27 19:13:11