You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Mongoose pre(save)钩子干扰save():邮箱验证报错原因排查

问题描述

我在开发电商项目的认证逻辑,用Mongoose结合validator校验userSchema字段,同时通过pre(save)钩子在持久化前哈希用户密码。

设置邮箱验证接口时,仅更新userSchema里的token和isVerified字段,完全没碰password相关字段,但调用接口时一直报错:

User validation failed: passwordConfirm: Please confirm your password

最后用findOneAndUpdate()绕开了问题,但想知道报错的根本原因。

相关代码

UserSchema 文件

const mongoose = require('mongoose');
const validator = require('validator');
const bcrypt = require('bcrypt');

const userSchema = new mongoose.Schema({
  name: {
    type: String,
    required: [true,'Please tell us your name']
  },

  email: {
    type: String,
    unique: true,
    lowercase: true,
    validate: [validator.isEmail, 'Please provide a valid email']
  },

  phone: {
    type: String,
  },

  password: {
    type: String,
    required: [true, 'Please provide a password'],
    minlength: 8,
    select: false
  },

  passwordConfirm: {
    type: String,
    required: [true, 'Please confirm your password'],
    validate: {
      // This only works on CREATE and SAVE!!!
      validator: function(el) {
        return el === this.password;
      },
      message: 'Passwords are not the same!'
    }
  },

  isVerified: {
    type: Boolean,
    default: false
  },

  token: String
});

userSchema.pre('save', async function(next) {
  if(!this.isModified('password')) return next();

  // Hash the password
  this.password = await bcrypt.hash(this.password, 10);
  next();

  // Delete PasswordConfirm field
  this.passwordConfirm = undefined;
});

// Instance methods
// Available on all documents in this collection
userSchema.methods.confirmPassword = async function
(enteredPassword, dbPassword) {
  return await bcrypt.compare(enteredPassword, dbPassword);
};

const User = mongoose.model('User', userSchema);

module.exports = User;

邮箱验证文件

exports.confirmEmail = async (req, res) => { 

const token = req.query.token;
console.log('This is the token: ', token); 
try { 
const decoded = await promisify(jwt.verify)(token, process.env.JWT_SECRET);
console.log('This is Decoded: ', decoded);

const user = await User.findOne({ email: decoded.payload.email });
console.log('This is the user: ', user);

// Checking if the token is still valid
if(decoded.exp * 1000 < Date.now() || !user){ 
console.log('Token is expired');
res.status(401).send('Token has expired'); return;
 };
user.token = undefined;
user.isVerified = true;
await user.save();

console.log('Email verified');
return res.status(200).json({ 
status: 'success', 
message: 'Email verified' }) }
catch (error) {
console.log(error.message);
res.status(500).json({ status:'failed', message: error.message }) } }
根本原因

问题出在passwordConfirm的必填校验和pre(save)钩子的执行顺序上:

  1. userSchema中passwordConfirm的required约束是全局生效的——每次调用save()时都会校验该字段是否存在,不管你有没有修改它。
  2. 用户创建完成后,passwordConfirm字段并不会存入数据库:因为你在pre(save)钩子中把它设为undefined,但这里的执行顺序错了——你先调用next()进入校验环节,之后才设置this.passwordConfirm = undefined,导致创建时这个字段就没被持久化。
  3. 当你从数据库查询出用户文档时,passwordConfirm字段根本不存在,此时调用user.save(),Mongoose会触发必填校验,发现缺少该字段,直接抛出错误。
为什么findOneAndUpdate()能规避?

Mongoose的findOneAndUpdate()默认不会触发Schema的校验规则,也不会执行pre(save)钩子(除非手动设置runValidators: true和context: 'query'),所以不会触发passwordConfirm的必填校验,自然不会报错。

优化方案

如果想继续使用save()方法,可做以下调整:

1. 修正pre(save)钩子的执行顺序

先删除passwordConfirm再调用next(),确保校验环节执行时该字段已被处理:

userSchema.pre('save', async function(next) {
  if(!this.isModified('password')) return next();

  // Hash the password
  this.password = await bcrypt.hash(this.password, 10);

  // 先删除字段,再进入下一个环节
  this.passwordConfirm = undefined;
  next();
});

2. 让passwordConfirm的必填约束仅在创建用户时生效

修改字段定义,通过this.isNew判断是否为新文档,只在创建时要求必填:

passwordConfirm: {
  type: String,
  required: function() {
    // 仅当创建新用户时才校验必填
    return this.isNew;
  },
  validate: {
    validator: function(el) {
      return el === this.password;
    },
    message: 'Passwords are not the same!'
  }
},

这样后续更新用户(比如邮箱验证)时,就不会触发passwordConfirm的必填校验了。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.05 13:08:21