使用Express和Mongoose更新密码时旧密码验证错误排查
密码更新控制器重复哈希问题排查与修复
问题描述
我实现了一个用户密码更新控制器,用户创建时数据库里的密码哈希是正确的,但第一次更新密码正常,后续用新密码作为旧密码再次更新时,会返回错误:"message": "Incorrect old password"。
现有代码
更新密码控制器
module.exports.updatePassword = async (req, res) => { const { userId } = req.params; const { oldPassword, newPassword } = req.body; try { const user = await User.findById(userId); const isPasswordValid = await user.isValidPassword(oldPassword); if (!isPasswordValid) { return res.status(401).json({ message: "Incorrect old password" }); } const salt = await bcrypt.genSalt(10); const newHashedPassword = await bcrypt.hash(newPassword, salt); const oldHashedPassword = user.password; if (oldHashedPassword === newHashedPassword) { return res .status(400) .json({ message: "New password should not be the same as old password", }); } user.password = newHashedPassword; await user.save(); return res.json({ message: "Password updated successfully" }); } catch (error) { console.error(error); return res.status(500).json({ message: "Server error" }); } };
用户Schema
const mongoose = require("mongoose"); const bcrypt = require("bcrypt"); const UserSchema = new mongoose.Schema({ email: { type: String, required: [true, "Provide an email."], unique: true, match: [ /^([\w-\.]+@([\w-]+\.)+[\w-]{2,4})?$/, "Please, provide a valid email.", ], }, password: { type: String, required: [true, "Password is required."], }, firstname: { type: String, required: [true, "Firstname is required."], }, lastname: { type: String, required: [true, "Lastname is required."], }, }); UserSchema.pre("save", async function (next) { const user = this; const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(user.password, salt); user.password = hash; next(); }); UserSchema.methods.isValidPassword = async function (password) { const user = this; const compare = await bcrypt.compare(password, user.password); return compare; }; module.exports = mongoose.model("User", UserSchema);
问题原因
核心问题在Schema的pre("save")中间件:每次调用user.save()时,中间件都会无条件对user.password进行哈希操作。
第一次更新密码时,你手动对新密码做了哈希,然后赋值给user.password,但调用save()时,中间件又把这个已经哈希过的字符串再哈希了一遍。最终数据库存储的是「哈希的哈希」值。
下次更新时,用户输入的旧密码是上次的明文新密码,用它去和数据库里的双重哈希值对比,自然匹配失败,返回"Incorrect old password"。
修复方案
1. 修改Schema中间件,仅在密码变更时哈希
修改pre("save")中间件,使用isModified()方法判断密码字段是否被修改,只有修改时才执行哈希操作:
UserSchema.pre("save", async function (next) { const user = this; // 仅当密码字段被修改时才重新哈希 if (user.isModified("password")) { const salt = await bcrypt.genSalt(10); const hash = await bcrypt.hash(user.password, salt); user.password = hash; } next(); });
2. 优化控制器代码(可选)
既然中间件会自动处理哈希,控制器里不需要手动哈希新密码,直接赋值明文即可,简化代码:
module.exports.updatePassword = async (req, res) => { const { userId } = req.params; const { oldPassword, newPassword } = req.body; try { const user = await User.findById(userId); const isPasswordValid = await user.isValidPassword(oldPassword); if (!isPasswordValid) { return res.status(401).json({ message: "Incorrect old password" }); } // 直接用bcrypt.compare对比明文,避免依赖数据库里的哈希值(防止中间件重复哈希导致的判断失效) const isSameAsNew = await bcrypt.compare(newPassword, user.password); if (isSameAsNew) { return res.status(400).json({ message: "New password should not be the same as old password" }); } user.password = newPassword; // 直接赋值明文,中间件会自动哈希 await user.save(); return res.json({ message: "Password updated successfully" }); } catch (error) { console.error(error); return res.status(500).json({ message: "Server error" }); } };
注意:原来控制器里用
oldHashedPassword === newHashedPassword判断新旧密码是否相同的逻辑,在中间件修复后也能正常工作,但用bcrypt.compare(newPassword, user.password)更可靠,因为即使中间件逻辑变化,也能正确对比明文和哈希值。
内容的提问来源于stack exchange,提问作者Johan
相关产品推荐
相关产品推荐

