Node.js中令牌创建的并发处理:如何避免重复请求新Access Token
如何处理并发401时只调用一次Token刷新接口?
这是个非常典型的并发场景问题,很多开发者在处理Token过期逻辑时都会碰到。核心目标就是避免多个请求同时触发Token刷新,造成不必要的资源浪费甚至冲突。下面给你几个实用的解决方案:
1. 用互斥锁+请求队列控制并发
这是最通用的思路,不管你是在前端还是后端实现,核心逻辑都是:
- 维护一个“正在刷新Token”的状态标记
- 当第一个请求触发401时,检查标记:如果未刷新,则发起刷新请求并标记为“正在刷新”
- 后续请求触发401时,看到标记为“正在刷新”,就加入等待队列,不重复发起刷新
- 刷新完成后,更新全局Token,然后批量重试队列里的所有请求
举个前端的伪代码例子(用JavaScript):
// 全局状态:是否正在刷新Token,以及等待重试的请求队列 let isRefreshing = false; let pendingRequests = []; async function handleTokenExpiry(originalRequest) { // 如果已经有刷新请求在进行,加入队列等待 if (isRefreshing) { return new Promise((resolve, reject) => { pendingRequests.push({ resolve, reject, request: originalRequest }); }); } isRefreshing = true; try { // 调用刷新Token接口 const newToken = await fetch('/api/refresh-token').then(res => res.json()); // 更新全局Token(比如存到localStorage或者请求头默认配置) localStorage.setItem('accessToken', newToken); // 重试所有排队的请求 pendingRequests.forEach(({ resolve, request }) => { // 给原请求换上新Token request.headers.Authorization = `Bearer ${newToken}`; resolve(fetch(request)); }); pendingRequests = []; // 清空队列 // 重试当前请求 originalRequest.headers.Authorization = `Bearer ${newToken}`; return fetch(originalRequest); } catch (error) { // 刷新失败,处理错误(比如跳转到登录页) pendingRequests.forEach(({ reject }) => reject(error)); pendingRequests = []; throw error; } finally { isRefreshing = false; // 重置刷新状态 } }
2. 借助请求库的拦截器统一处理
如果你的项目用了像Axios、Retrofit这类带拦截器的请求库,可以把这个逻辑封装到响应拦截器里,让所有请求自动处理401和Token刷新,不用每个请求单独写逻辑。
比如Axios的响应拦截器实现:
const axios = require('axios'); const instance = axios.create({ baseURL: '/api' }); let isRefreshing = false; let pendingRequests = []; instance.interceptors.response.use( response => response, async error => { const originalRequest = error.config; // 只处理未重试过的401请求,避免无限循环 if (error.response.status === 401 && !originalRequest._retry) { originalRequest._retry = true; if (isRefreshing) { // 加入等待队列 return new Promise((resolve, reject) => { pendingRequests.push({ resolve, reject, req: originalRequest }); }).then(newToken => { originalRequest.headers.Authorization = `Bearer ${newToken}`; return instance(originalRequest); }).catch(err => Promise.reject(err)); } isRefreshing = true; try { const { data: newToken } = await instance.post('/refresh-token'); instance.defaults.headers.common['Authorization'] = `Bearer ${newToken}`; // 重试队列请求 pendingRequests.forEach(({ resolve, req }) => { req.headers.Authorization = `Bearer ${newToken}`; resolve(instance(req)); }); pendingRequests = []; return instance(originalRequest); } catch (refreshErr) { pendingRequests.forEach(({ reject }) => reject(refreshErr)); pendingRequests = []; // 这里可以跳转到登录页或者清空Token window.location.href = '/login'; return Promise.reject(refreshErr); } finally { isRefreshing = false; } } return Promise.reject(error); } );
3. 后端辅助优化:Token宽限期
除了前端的并发控制,后端可以给Token设置一个“宽限期”——比如Token过期后5分钟内,旧Token仍然可以用于请求,同时后端在响应头里返回新的Token。这样客户端可以提前刷新Token,减少并发401的概率。不过这个是辅助手段,还是需要配合前面的并发控制逻辑,因为极端情况下还是可能出现多个401请求。
总结
最核心的就是用状态标记控制刷新请求的唯一性,用队列缓存等待的请求,确保同一时间只有一个Token刷新请求发出,其他请求复用刷新后的Token。这种方式不管是前端还是后端都适用,逻辑清晰且容易维护。
内容的提问来源于stack exchange,提问作者Omar El Malak
相关产品推荐
相关产品推荐

