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

Node.js+Express邮箱验证后密码异常致无法登录问题求助

问题描述

使用Node.js + Express构建API认证路由,注册、登录功能正常,但完成邮箱验证后用户密码被修改,导致无法登录。仅在使用bcrypt加密时出现此问题,不加密则一切正常。

相关代码

用户模型

import { Schema, model } from "mongoose";
import bcrypt from 'bcrypt';

const UserSchema = new Schema({
    Admin: {
        type: Boolean,
        default: false
    },
    name: {
        type: String,
        required: [true, 'name field required!']
    },
    email: {
        type: String,
        required: [true, 'email field required'],
        unique: [true, 'email already taken!']
    },
    password: {
        type: String,
        required: [true, 'password field required']
    },
    verified: {
        type: Boolean,
        default: false
    },
    verificationToken: {
        type: String
    },
    addresses: [
        {
            name: String,
            mobileNo: String,
            houseNo: String,
            streetNo: String,
            landMark: String,
            city: String,
            country: String,
            postalCode: String
        }
    ],
    orders: [
        {
            type: Schema.Types.ObjectId,
            ref: 'Order'
        }
    ]
}, { timestamps: true });

/** 保存文档前执行函数 */
UserSchema.pre('save', async function (next) {
    const salt = await bcrypt.genSalt();
    this.password = await bcrypt.hash(this.password, salt);
    next();
});

/** 保存文档后执行函数 */
UserSchema.post("save", async (doc, next) => {
    console.log("用户已创建!", doc);
    next();
});

/** 用户登录执行函数 */
UserSchema.statics.login = async function (email, password) {
    const user = await this.findOne({ email });
    
    if (user) {
        const auth = await bcrypt.compare(password, user.password);

        if (auth) {
            return user;
        }
        throw Error('Wrong password!')
    }

    throw Error('Email does not exist!')
}


const User = model('User', UserSchema);

export default User;

控制器

import crypto from 'crypto';
import User from "../models/User.js";
import sendEmailVerification from '../lib/nodemailer.js';
import { createToken, maxAge } from "../lib/token.js";

/** 
 * ! 注册新用户
 */
const registerUser = async (req, res) => {
    try {
        const { name, email, password } = req.body;

        /** 检查用户是否已存在 */
        const userExists = await User.findOne({ email });

        if (userExists) {
            return res.status(401).json({message: 'User already exists!'})
        }


        const user = await User.create({
            name,
            email,
            password,
            verificationToken: crypto.randomBytes(20).toString('hex')
        });

        /** 发送验证邮件 */
        await sendEmailVerification(user.email, user.verificationToken);

        return res.status(200).json({message: 'User registration successful'})
    } catch (error) {
        console.log(error);
        return res.status(500).json({error: error.message})
    }
}

/**
 * ? 验证令牌
 */
const verifyToken = async (req, res) => {
    try {
        const { token } = req.params;

        /** 根据验证令牌查找用户 */
        const user = await User.findOne({ verificationToken: token });
        if (!user) {
            return res.status(401).json({message: 'Invalid verification token!'})
        }

        /** 标记为已验证 */
        user.verified = true;
        user.verificationToken = undefined;

        await user.save();

        res.status(200).json({message: 'User token has been verified!'})
    } catch (error) {
        console.log(error);
        return res.status(500).json({message: 'Token verification failed!'})
    }
}


/**
 * ! 用户登录;
 */
const loginUser = async (req, res) => {
    try {
        const { email, password } = req.body;

        /** 检查用户是否已验证 */
        const user = await User.login(email, password);

        const token = await createToken(user._id, user.email, user.Admin, user.name);
        res.cookie("authToken", token, { maxAge: maxAge * 1000, httpOnly: true})
        res.status(200).json({ user });
    } catch (error) {
        console.log(error);
        return res.status(500).json({error: error.message})
    }
}

export {
    registerUser,
    verifyToken,
    loginUser
}

路由

import { Router } from "express";
import { loginUser, registerUser, verifyToken } from "../controllers/auth.js";


const router = Router();

/**
 * ! 创建新用户
 */
router.post("/register", registerUser);

/**
 * ? 验证令牌
 */
router.get('/verify/:token', verifyToken);

/**
 * ! 用户登录;
 */
router.post("/login", loginUser);

export default router;
解决方案

问题出在pre('save')钩子:每次调用user.save()时(包括邮箱验证标记用户为已验证时),都会重新对密码进行哈希,导致原本的哈希密码被再次加密,登录时自然匹配失败。

修改用户模型中的pre('save')钩子,仅当密码字段被修改时才执行哈希操作:

UserSchema.pre('save', async function (next) {
    // 只有密码被修改时才重新哈希
    if (!this.isModified('password')) return next();
    
    const salt = await bcrypt.genSalt();
    this.password = await bcrypt.hash(this.password, salt);
    next();
});

解释:this.isModified('password')会判断密码字段是否有变更,用户注册时密码是新的,会执行哈希;邮箱验证时只修改verified和verificationToken字段,密码未变更,跳过哈希操作,避免重复加密。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 01:35:59