微信小程序双Token无感刷新高并发场景下的请求队列与竞态控制实战在微信小程序开发中用户身份验证通常依赖于后端颁发的Token。然而Token过期后的处理一直是前端工程化的难点——如何在用户无感知的情况下完成Token刷新特别是在高并发场景下多个请求同时触发401时如何避免重复刷新和请求丢失本文将深入探讨基于双Token机制的完整解决方案。1. 双Token机制的设计原理传统单Token方案最大的痛点在于当Token过期时必须中断用户操作跳转登录页。而双Token机制通过引入access_token短期有效和refresh_token长期有效两个令牌实现了无感刷新的可能。核心工作流程用户首次登录获取双Token例如access_token有效期2小时refresh_token有效期7天每次请求携带access_token当access_token过期返回401时使用refresh_token获取新的access_token用新Token重试原始请求当refresh_token也过期时才要求用户重新登录// 登录接口返回示例 { access_token: eyJhb..., expires_in: 7200, refresh_token: eyJhb..., user_info: {...} }关键点refresh_token的有效期应显著长于access_token但也不宜过长通常7-30天。过短会导致频繁登录过长则增加安全风险。2. 高并发场景下的挑战与解决方案当多个请求同时触发401时原始方案会出现严重问题问题场景原始方案缺陷优化方案并行请求触发401每个请求都尝试刷新Token导致重复刷新引入isRefreshing标志位刷新期间新请求新请求因Token过期直接失败建立请求队列暂存待处理请求刷新失败处理无统一错误处理流程全局错误处理跳转登录页核心状态管理变量let isRefreshing false; // 刷新锁 let requestQueue []; // 请求等待队列 const MAX_RETRY 2; // 最大重试次数3. 完整请求类实现以下是支持高并发的请求类完整实现关键功能包括自动Token刷新请求队列管理错误重试机制并发控制class Http { constructor() { this.baseURL https://api.yourservice.com; this.timeout 10000; } async request(options) { const { url, method GET, data, needAuth true } options; // 1. 构造请求头 const header { Content-Type: application/json }; if (needAuth) { const token wx.getStorageSync(access_token); if (token) header.Authorization Bearer ${token}; } // 2. 发起请求 return new Promise((resolve, reject) { wx.request({ url: ${this.baseURL}${url}, method, data, header, success: (res) this._handleResponse(res, { resolve, reject, ...options }), fail: (err) this._handleError(err, reject), complete: () wx.hideLoading() }); }); } _handleResponse(res, ctx) { // 3. 成功响应 if (res.statusCode 200 res.statusCode 300) { return ctx.resolve(res.data); } // 4. Token过期处理 if (res.statusCode 401) { return this._handleTokenExpired(ctx); } // 5. 其他错误 this._showError(res.data?.message || 请求失败); return ctx.reject(res); } async _handleTokenExpired(ctx) { // 6. 如果正在刷新将请求加入队列 if (isRefreshing) { return new Promise(resolve { requestQueue.push(() this.request(ctx).then(resolve)); }); } // 7. 加锁并开始刷新 isRefreshing true; try { const newToken await this._refreshToken(); wx.setStorageSync(access_token, newToken); // 8. 重试原始请求 const result await this.request(ctx); // 9. 处理队列中的请求 await this._processQueue(); return ctx.resolve(result); } catch (err) { this._clearAuth(); return ctx.reject(err); } finally { isRefreshing false; } } async _refreshToken() { const refreshToken wx.getStorageSync(refresh_token); if (!refreshToken) throw new Error(请重新登录); const res await wx.request({ url: ${this.baseURL}/auth/refresh, method: POST, data: { refresh_token: refreshToken } }); if (res.statusCode 200) { wx.setStorageSync(access_token, res.data.access_token); wx.setStorageSync(refresh_token, res.data.refresh_token); return res.data.access_token; } throw new Error(res.data?.message || 刷新Token失败); } async _processQueue() { while (requestQueue.length) { const retry requestQueue.shift(); try { await retry(); } catch (err) { console.error(队列请求重试失败:, err); } } } _clearAuth() { wx.removeStorageSync(access_token); wx.removeStorageSync(refresh_token); wx.navigateTo({ url: /pages/login/login }); } _showError(msg) { wx.showToast({ title: msg, icon: none }); } } export default new Http();4. 关键流程的时序控制为了更清晰理解高并发场景下的处理流程以下是典型场景的时序图第一个请求触发401设置isRefreshing true发起刷新Token请求将后续请求加入队列刷新成功后的处理更新本地Token重试原始请求依次处理队列中的请求刷新失败的处理清除本地Token跳转登录页清空请求队列sequenceDiagram participant Client participant HttpService participant Backend Client-HttpService: 请求AToken过期 HttpService-Backend: 返回401 HttpService-HttpService: 标记isRefreshingtrue HttpService-Backend: 发起刷新请求 Client-HttpService: 请求B并发 HttpService-HttpService: 加入请求队列 Backend--HttpService: 返回新Token HttpService-Backend: 重试请求A HttpService-Backend: 处理队列请求B Backend--HttpService: 返回请求A结果 Backend--HttpService: 返回请求B结果 HttpService--Client: 返回最终结果5. 性能优化与边界情况处理在实际项目中还需要考虑以下优化点内存管理设置队列最大长度如100条超时请求自动移除页面卸载时清空队列// 增强版的队列处理 const MAX_QUEUE_SIZE 100; const REQUEST_TIMEOUT 30000; function addToQueue(request) { if (requestQueue.length MAX_QUEUE_SIZE) { requestQueue.shift(); // 移除最旧请求 } const timer setTimeout(() { const index requestQueue.indexOf(request); if (index -1) requestQueue.splice(index, 1); }, REQUEST_TIMEOUT); requestQueue.push(async () { clearTimeout(timer); return request(); }); }错误重试策略指数退避重试关键请求特殊处理网络状态检测async function requestWithRetry(options, retries 3) { try { return await http.request(options); } catch (err) { if (retries 0 || !this._shouldRetry(err)) throw err; const delay Math.pow(2, 3 - retries) * 1000; await new Promise(resolve setTimeout(resolve, delay)); return this.requestWithRetry(options, retries - 1); } } _shouldRetry(err) { // 仅重试网络错误和5xx错误 return !err.response || (err.response.status 500 err.response.status 599); }6. 安全增强措施Token机制需要特别注意安全防护HttpOnly Cookies如果使用WebView优先考虑Cookie存储Token绑定将Token与设备指纹绑定使用限制refresh_token单次使用后失效监控告警异常频繁刷新时触发安全预警// 示例设备指纹生成 function getDeviceFingerprint() { const systemInfo wx.getSystemInfoSync(); return md5( systemInfo.model systemInfo.system systemInfo.platform ); } // 刷新请求携带设备信息 _refreshToken() { const fingerprint getDeviceFingerprint(); return wx.request({ url: ${this.baseURL}/auth/refresh, data: { refresh_token: wx.getStorageSync(refresh_token), device_id: fingerprint } }); }7. 实际项目中的经验总结在电商类小程序中应用此方案后登录中断率从12%降至0.3%。以下是一些实战建议Token有效期设置生产环境access_token2-4小时refresh_token7-30天测试环境缩短至5-10分钟方便测试性能监控指标// 记录关键指标 wx.reportAnalytics(token_refresh, { success: true, duration: Date.now() - startTime, queue_size: requestQueue.length });用户体验优化刷新过程中显示轻量级提示关键操作如支付提供手动刷新按钮网络恢复后自动处理积压请求服务端配合提供Token吊销接口支持多端登录管理实时推送Token失效通知通过这套完整的双Token无感刷新方案开发者可以显著提升小程序的用户体验和系统健壮性。特别是在高并发场景下合理的队列管理和竞态控制能够保证业务逻辑的可靠执行。