Mongoose预保存钩子导致登录密码验证失败求助
密码哈希后登录验证失败,bcrypt.compare返回false的排查与解决
未添加Mongoose pre save钩子前登录API正常运行,添加该钩子实现密码哈希后,登录时持续提示“密码错误”。日志显示注册时存入的哈希密码和登录读取的完全一致,但bcrypt.compare()始终返回false,多次尝试无法解决。
登录API代码
export const userLogin = async (req, res) => { try { const { email, password } = req.body; console.log("Received Email:", email); console.log("Received Password:", password); const user = await User.findOne({ email }); console.log("Stored Hashed Password:", user.password); if (!user) { return res.status(200).send({ message: "Email is not registered", success: false, }); } const checkPassword = await bcrypt.compare(password, user.password); console.log("Password Comparison Result:", checkPassword); if (!checkPassword) { return res.status(200).send({ message: "Password is wrong", success: false, }); } else { let token = jwt.sign({ id: user._id }, process.env.JWT_SECRET, { expiresIn: "1d", }); res.status(200).send({ message: "Login Succes", success: true, data: token, }); } } catch (error) { return res.status(500).send({ message: "Internal Server Error", success: false, }); } };
Mongoose模型及pre save钩子代码
import mongoose from "mongoose"; import bcrypt from "bcryptjs"; const schema = mongoose.Schema; const userSchema = new schema( { firstName: { type: String, required: true, }, lastName: { type: String, required: true, }, userName: { type: String, required: true, }, email: { type: String, required: true, unique: true, match: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, }, dob: { type: Date, }, city: { type: String, }, password: { type: String, required: true, }, newPassword: { type: String, }, role: { type: String, default: "user", }, otpCode: { type: Number, }, }, { timestamps: true } ); // Verify Password Method userSchema.methods.verifyPassword = function (password) { return bcrypt.compareSync(password, this.password); }; // Get Password Hash Method userSchema.methods.getPasswordHash = async function (password) { const rounds = await bcrypt.genSalt(10); const hash = await bcrypt.hash(password, rounds); return hash; }; // Pre-save Hook userSchema.pre("save", async function (next) { console.log("pre hook: validate username"); try { const regex = /^[a-zA-Z0-9]+$/; const isUsernameValid = regex.test(this.userName); if (!isUsernameValid) { console.log("this will stop execution of next middlewares"); throw new Error("username is not alphanumeric"); } if (!this.isModified("password")) return next(); const trimmedPassword = this.password.trim(); const rounds = await bcrypt.genSalt(10); const hash = await bcrypt.hash(this.password, rounds); this.password = hash; console.log("database hashed password", this.password); return next(); } catch (error) { console.error(error.message); throw error; } }); const userModal = new mongoose.model("users", userSchema); export default userModal;
问题排查与解决方案
核心问题点
- 钩子中定义了
trimmedPassword = this.password.trim(),但实际哈希时仍使用未修剪的this.password。如果用户注册时密码前后带空格,哈希的是带空格的原始密码,而登录时用户输入无空格的密码,对比必然失败。 - 潜在风险:若钩子因其他字段更新重复触发,可能对已哈希的密码再次哈希,导致存储的哈希值与原始密码完全不匹配。
修复步骤
- 修正pre save钩子的密码处理逻辑:确保使用修剪后的密码进行哈希
// Pre-save Hook userSchema.pre("save", async function (next) { console.log("pre hook: validate username"); try { const regex = /^[a-zA-Z0-9]+$/; const isUsernameValid = regex.test(this.userName); if (!isUsernameValid) { console.log("this will stop execution of next middlewares"); throw new Error("username is not alphanumeric"); } if (!this.isModified("password")) return next(); // 使用修剪后的密码哈希 const trimmedPassword = this.password.trim(); const rounds = await bcrypt.genSalt(10); const hash = await bcrypt.hash(trimmedPassword, rounds); this.password = hash; console.log("database hashed password", this.password); return next(); } catch (error) { console.error(error.message); throw error; } });
- 清理错误数据:删除数据库中已注册的用户数据(这些数据的密码已被错误哈希),重新注册测试。
- 前端/注册接口优化:在注册环节提前对密码进行修剪,避免前端传入的空格引发后续问题,同时确保
isModified("password")的判断准确性,防止重复哈希。
内容的提问来源于stack exchange,提问作者Fraz Khan
相关产品推荐
相关产品推荐

