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

React+Express项目refresh_token重复请求日志异常问题排查

问题分析

核心问题出在前端无并发控制的重复刷新请求,以及后端RefreshToken接口的非原子性操作:

  1. 当首页多个请求同时检测到access_token过期时,前端会发起多个/auth/refresh_token请求;
  2. 后端接口先执行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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.03 09:35:22