如何在JavaScript客户端实现多Access Token过期自动刷新功能?
多Access Token自动刷新的实现思路与方案
我之前刚好做过多Token场景下的自动刷新功能,踩过一些坑,分享下我的经验和可行方案,应该能解决你的困惑。
首先说下为什么直接用多个setTimeout会麻烦:每个Token对应一个定时器,你得手动跟踪所有定时器ID,更新Token时还要记得清除旧的定时器,很容易漏处理导致重复刷新或者失效的定时器残留。而你提到的订阅模式其实可行,但对这个场景来说,有更简洁的两种方案可以选择:
方案一:全局轮询+统一Token状态管理
这个方案适合Token数量较多的情况,不用给每个Token单独设定时器,而是定期检查所有Token的过期时间,判断是否需要刷新。
核心实现思路:
- 用一个
TokenManager类统一存储所有Token的信息(包括accessToken、refreshToken、过期时间、受众标识audience),还要加一个isRefreshing标记防止同一Token重复触发刷新请求。 - 启动一个全局的定时任务(比如每隔10秒),遍历所有Token,计算距离过期还有多久,提前30秒触发刷新(避免刚好在过期时发送请求失败)。
- 刷新成功后更新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时清除旧定时器。
核心实现思路:
- 每个Token添加时,计算刷新时间,设置
setTimeout,并把定时器ID存在Token信息里。 - 当Token更新(比如刷新成功),先清除旧的定时器,再设置新的定时器。
- 同样加入
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等)和方案一一致 }
关键注意点
- 避免并发刷新:一定要加
isRefreshing标记,防止同一Token被多个定时器触发多次刷新请求。 - 刷新失败处理:网络波动或者Refresh Token过期都会导致刷新失败,需要添加重试机制,或者在多次失败后提示用户重新登录。
- Token持久化:页面刷新后Token会丢失,所以要把Token存在
localStorage或sessionStorage,加载时恢复。 - 提前刷新缓冲:不要等到Token过期再刷新,提前30-60秒刷新,确保新Token在旧Token过期前已经可用。
内容的提问来源于stack exchange,提问作者Juanma Perez
相关产品推荐
相关产品推荐

