You need to enable JavaScript to run this app. JWT刷新access token后req.user丢失用户核心字段的问题求助
优惠活动
大模型
产品
解决方案
定价
更多

JWT刷新access token后req.user丢失用户核心字段的问题求助

相关代码片段

JWT身份认证中间件

module.exports.authenticateToken = (req, res, next) => {
  const authHeader = req.headers["authorization"];
  const token = authHeader && authHeader.split(" ")[1];

  if (!token) {
    return res.status(401).json({ success: false, msg: "not authorized" });
  }

  jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => {
    if (err) {
      return res
        .status(403)
        .json({ success: false, msg: "jwt cannot be verified" });
    }
    req.user = user;
    next();
  });
};

双令牌生成逻辑

module.exports.generateAccessToken = (user) => {
  const payload = {
    sub: user._id,
    name: user.username,
  };

  return jwt.sign(payload, process.env.ACCESS_TOKEN_SECRET, {
    expiresIn: "20s",
  });
};

module.exports.generateRefreshToken = (user) => {
  const payload = {
    sub: user._id,
    name: user.username,
  };

  return jwt.sign(payload, process.env.REFRESH_TOKEN_SECRET);
}

用户登录接口逻辑

注:目前调试阶段将refresh token临时存储在内存数组中,流程跑通后会更换为正式存储方案

module.exports.postAuthLogin = async (req, res) => {
  const { username, password } = req.body;

  try {
    const user = await User.findOne({ username });

    if (!user) {
      return res.status(401).json({ success: false, msg: "Invalid username" });
    }

    const passwordIsValid = validatePasswordHash(
      password,
      user.salt,
      user.hash
    );

    if (!passwordIsValid) {
      return res.status(401).json({ success: false, msg: "Invalid password" });
    }

    const accessToken = generateAccessToken(user);
    const refreshToken = generateRefreshToken(user);

    refreshTokens.push(refreshToken);
    res.status(200).json({ success: true, accessToken, refreshToken });
  } catch (err) {
    return res.json({ msg: err.toString() });
  }
};

令牌刷新接口逻辑

module.exports.postAuthRefreshToken = (req, res) => {
  const { refreshToken } = req.body;

  if (!refreshToken) {
    return res
      .status(401)
      .json({ success: false, msg: "must provide a refresh token" });
  }

  if (!refreshTokens.includes(refreshToken)) {
    return res.status(403).json({
      success: false,
      msg: "refresh token was deleted you must login again",
    });
  }

  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    const accessToken = generateAccessToken(user);
    res.status(200).json({ success: true, accessToken });
  });
};
故障现象
  • 初始登录完成后,携带首次生成的access token请求受中间件保护的路由,路由中解析到的req.user包含完整业务字段:
    {
      sub: '5ff5f16f4203ade4d1b63da8',
      name: 'test1',
      iat: 1610055109,
      exp: 1610055129
    }
    
  • access token过期后,调用刷新接口获取新的access token,即便新令牌能正常通过中间件校验、成功访问受保护路由,解析到的req.user会丢失全部业务字段,仅保留JWT默认生成的时间戳:
    { iat: 1610055143, exp: 1610055163 }
    
问题根因与修复方案

根因

问题出在令牌刷新接口的JWT验证回调逻辑,存在两个叠加的错误:

  • jwt.verify回调中完全省略了错误判断分支,即便refresh token校验失败,也会继续执行生成新access token的流程。
  • generateAccessToken函数的入参要求是携带_id、username字段的用户对象,但在刷新流程中,直接把JWT解码得到的refresh token payload(字段为sub、name、自身的时间戳字段)传给了该函数。由于解码出的对象不存在_id、username属性,构造access token payload时,sub、name字段的值均为undefined,而JWT序列化时会自动忽略值为undefined的字段,最终生成的access token就只剩下默认生成的iat、exp两个时间戳字段。

修复方式

  1. 补上refresh token校验的错误判断逻辑,校验失败时直接返回403响应,不再执行后续令牌生成流程。
  2. 传入generateAccessToken的参数必须匹配函数的字段要求:可以从refresh token解出的sub字段查询完整数据库用户对象,也可以手动从解码payload中提取字段,构造符合入参要求的对象。

修复后的刷新接口核心逻辑参考:

jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, decodedRefreshUser) => {
  // 校验失败直接返回
  if (err) {
    return res.status(403).json({
      success: false,
      msg: "refresh token invalid"
    })
  }
  // 构造符合generateAccessToken入参要求的对象
  const userPayload = {
    _id: decodedRefreshUser.sub,
    username: decodedRefreshUser.name
  }
  const accessToken = generateAccessToken(userPayload);
  res.status(200).json({ success: true, accessToken });
});

内容的提问来源于stack exchange,提问作者INuke

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.11.10 05:36:04