Node.js+React+MongoDB登录JWT更新异常:首次无Token问题排查
我基于Node.js Express后端开发React网站的登录功能,需求是用户登录时生成JWT Token并更新至MongoDB数据库,前端验证后将Token存入localStorage。但注册后首次登录,返回结果中无Token导致无法登录,再次点击登录按钮才会生成并更新Token。明明在resolve中返回findOneAndUpdate的结果,为何首次响应只返回findOne查询到的用户数据?
相关代码如下:
Auth Controller
login(params) { params.email = params.email.toLowerCase(); return new Promise((resolve, reject) => { db.collection("Users").findOne({ email: params.email }).then((response) => { console.log(response) if(response) { bcrypt.compare(params.password, response.password, (err, success) => { if(success) { let token = jwt.sign({ name: response.name, id: response._id }, proccess.env.JWT_SECRET); // 注意拼写错误:proccess → process db.collection("Users").findOneAndUpdate({ email: params.email }, { $set: { token: token, lastLogin: new Date() }, }, function (e, s) { if(e) { console.log(e) reject(e) } else { console.log("updated") resolve(s) } }) } else { reject({msg: 'Incorrect email or password.'}) } }) } else { reject({msg: 'cannot log in user'}); } }) }) }
Auth Router
router.post('/login', (req, res) => { let User = new models.User() let processes = []; processes.push(function (callback) { User.login(req.body).then(function (response) { callback(null, response); }, function (error) { console.log(error) callback(error); }); }); async.waterfall(processes, function (error, data) { if (!error) { return res.json({ statusCode: 200, msg: 'User logged in successfully.', result: data }); } else { return res.json({ statusCode: 401, msg: 'Cannot login user.', error: error }); } }); })
React Login.js
const login = () => { axios.post('/login', data).then(async (response) => { console.log(response) if(response && response.data.result.value.token ) { localStorage.setItem("authUser", JSON.stringify(response.data.result.value.token)) history.push("/") console.log(response.data.result) } else { console.log("ERROR") } }) }
核心原因
MongoDB的findOneAndUpdate方法默认返回更新前的文档。首次登录时,数据库里的用户文档还没有token字段,所以返回的旧文档自然不带token;第二次登录时,数据库已经有了上次更新的token,返回的旧文档里包含该token,所以前端能正常获取。
修复步骤
修改findOneAndUpdate参数,返回更新后的文档
根据你使用的MongoDB驱动版本,添加对应的参数:- 驱动4.0及以上版本:添加
returnDocument: 'after' - 旧版本驱动:添加
returnNewDocument: true
修改后的
findOneAndUpdate代码:db.collection("Users").findOneAndUpdate( { email: params.email }, { $set: { token: token, lastLogin: new Date() } }, { returnDocument: 'after' }, // 关键参数:返回更新后的文档 function (e, s) { if(e) { console.log(e) reject(e) } else { console.log("updated") resolve(s) } } )- 驱动4.0及以上版本:添加
修正拼写错误
代码中proccess.env.JWT_SECRET的proccess是拼写错误,应改为process.env.JWT_SECRET,否则会导致JWT签名失败。优化代码(可选)
把回调式的代码改成Promise/async-await风格,避免回调地狱,代码更易维护:async login(params) { params.email = params.email.toLowerCase(); const user = await db.collection("Users").findOne({ email: params.email }); if (!user) { throw { msg: 'cannot log in user' }; } const isPasswordValid = await bcrypt.compare(params.password, user.password); if (!isPasswordValid) { throw { msg: 'Incorrect email or password.' }; } const token = jwt.sign( { name: user.name, id: user._id }, process.env.JWT_SECRET ); const updatedUser = await db.collection("Users").findOneAndUpdate( { email: params.email }, { $set: { token, lastLogin: new Date() } }, { returnDocument: 'after' } ); return updatedUser; }
前端验证调整(可选)
确保前端能正确识别更新后的文档结构,比如response.data.result.token(根据驱动返回的结构调整,有些版本返回的是response.data.result.value)。
内容的提问来源于stack exchange,提问作者motionless570

