React+Express项目refresh_token重复请求日志异常问题排查
问题分析
核心问题出在前端无并发控制的重复刷新请求,以及后端RefreshToken接口的非原子性操作:
- 当首页多个请求同时检测到access_token过期时,前端会发起多个
/auth/refresh_token请求; - 后端接口先执行
findOneAndDelete删除旧RefreshToken,并发请求中第二个请求会直接找不到旧Token,但第一个请求的响应流程被打乱,导致morgan无法捕获到正确的响应状态、文本和耗时,出现-的异常日志。
解决方案
前端:添加刷新令牌的并发锁
修改axios拦截器,用全局Promise存储正在进行的刷新请求,让后续请求复用同一刷新结果,避免重复发起请求:
// 全局变量:存储正在进行的刷新请求Promise let refreshTask = null; axios.interceptors.request.use(async function (config) { const token = localStorage.getItem("accessToken"); if (token) { const decoded = jwt_decode(token); // 检测token是否过期 if (Date.now() >= decoded.exp * 1000) { // 无刷新任务时,发起新的刷新请求 if (!refreshTask) { const refreshAxios = axios.create({}); refreshTask = refreshAxios .post( "http://localhost:3000/auth/refresh_token", {}, { headers: { token: localStorage.getItem("refreshToken") } } ) .then(res => { const { accessToken, refreshToken } = res.data.data; localStorage.setItem("accessToken", accessToken); localStorage.setItem("refreshToken", refreshToken); return accessToken; }) .catch(err => { console.error(err); refreshTask = null; throw err; }); } // 等待刷新完成,复用新token const newToken = await refreshTask; config.headers.Authorization = `Bearer ${newToken}`; refreshTask = null; } else { config.headers.Authorization = `Bearer ${token}`; } } return config; });
后端:用原子操作优化RefreshToken逻辑
将「删除旧Token+插入新Token」的两步操作改为原子性的findOneAndUpdate,避免并发请求导致的状态不一致:
exports.refreshToken = async (req, res) => { const requestErrors = validate(req); if (!requestErrors.isEmpty()) { return res.error(requestErrors.array()[0]); } const refreshToken = req.headers.token; // 先验证旧Token是否存在 const dbToken = await RefreshToken.findOne({ token: refreshToken }); if (!dbToken) { return res.error({ error: "Invalid Token" }); } // 生成新令牌 const payload = { id: dbToken.user }; const newAccessToken = jwt.generateToken(payload, "access"); const newRefreshToken = jwt.generateToken(payload, "refresh"); // 原子替换旧Token:只有原Token存在时才更新 const updatedToken = await RefreshToken.findOneAndUpdate( { token: refreshToken }, { token: newRefreshToken }, { new: true } ); if (!updatedToken) { return res.error({ error: "Token already refreshed" }); } return res.success({ accessToken: newAccessToken, refreshToken: newRefreshToken, }); };
额外检查:确保res.error和res.success自定义方法正确设置了HTTP响应状态码(比如res.success设为200,res.error设为401/400等),morgan需要正确的响应状态才能生成正常日志。
内容的提问来源于stack exchange,提问作者alex
相关产品推荐
相关产品推荐

