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两个时间戳字段。
修复方式
- 补上refresh token校验的错误判断逻辑,校验失败时直接返回403响应,不再执行后续令牌生成流程。
- 传入
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
相关产品推荐
相关产品推荐

