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

MERN栈用户验证异常:仅首个注册用户可完成验证与密码重置

问题场景

注册user1时,验证邮件成功发送至对应邮箱,user1可完成邮箱验证并正常修改密码;但注册user2后,虽验证邮件已发送,点击验证链接却显示无效,仅首个注册用户可完成完整的验证流程。

技术栈

基于MERN栈开发

后端代码

Backend => route.js

const express = require("express");
const router = express.Router();
const User = require("../models/userModel");
const Doctor = require("../models/doctorModel");
const bcrypt = require("bcryptjs");
const jwt = require("jsonwebtoken");
const authMiddleware = require("../middlewares/authMiddleware");
const sendEmail = require("../utils/sendMail");
const Token = require("../models/tokenModel");
const Appointment = require("../models/appointmentModel");
const moment = require("moment");

router.post("/register", async (req, res) => {
  try {
    const userExists = await User.findOne({ email: req.body.email });
    if (userExists) {
      return res
        .status(200)
        .send({ message: "User already exists", success: false });
    }
    const password = req.body.password;
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);
    req.body.password = hashedPassword;
    const newuser = new User(req.body);
    const result = await newuser.save();
    await sendEmail(result, "verifyemail");
    res
      .status(200)
      .send({ message: "User created successfully", success: true });
  } catch (error) {
    console.log(error);
    res
      .status(500)
      .send({ message: "Error creating user", success: false, error });
  }
});

router.post("/login", async (req, res) => {
  try {
    const result = await User.findOne({ data: req.body.userId });
    console.log(result);
    const user = await User.findOne({ email: req.body.email });

    if (!user) {
      return res
        .status(200)
        .send({ message: "User does not exist", success: false });
    }
    if (user.isVerified === false) {
      return res
        .status(200)
        .send({ message: "User not Verified", success: false });
    }
    const isMatch = await bcrypt.compare(req.body.password, user.password);
    if (!isMatch) {
      return res
        .status(200)
        .send({ message: "Password is incorrect", success: false });
    } else {
      const dataToBeSentToFrontend = {
        id: user._id,
        email: user.email,
        name: user.name,
      };
      const token = jwt.sign(dataToBeSentToFrontend, process.env.JWT_SECRET, {
        expiresIn: "1d",
      });

      res
        .status(200)
        .send({ message: "Login successful", success: true, data: token });
    }
  } catch (error) {
    console.log(error);
    res
      .status(500)
      .send({ message: "Error logging in", success: false, error });
  }
});

router.post("/get-user-info-by-id", authMiddleware, async (req, res) => {
  try {
    const user = await User.findOne({ _id: req.body.userId });
    user.password = undefined;
    if (!user) {
      return res
        .status(200)
        .send({ message: "User does not exist", success: false });
    } else {
      res.status(200).send({
        success: true,
        data: user,
      });
    }
  } catch (error) {
    res
      .status(500)
      .send({ message: "Error getting user info", success: false, error });
  }
});

router.post("/send-password-reset-link", async (req, res) => {
  try {
    const result = await User.findOne({ email: req.body.email });
    await sendEmail(result, "resetpassword");
    res.send({
      success: true,
      message: "Password reset link sent to your email successfully",
    });
  } catch (error) {
    res.status(500).send(error);
  }
});

router.post("/resetpassword", async (req, res) => {
  try {
    const tokenData = await Token.findOne({ token: req.body.token });
    if (tokenData) {
      const password = req.body.password;
      const salt = await bcrypt.genSalt(10);
      const hashedPassword = await bcrypt.hash(password, salt);
      await User.findOneAndUpdate({
        _id: tokenData.userid,
        password: hashedPassword,
      });
      await Token.findOneAndDelete({ token: req.body.token });
      res.send({ success: true, message: "Password reset successfull" });
    } else {
      res.send({ success: false, message: "Invalid token" });
    }
  } catch (error) {
    res.status(500).send(error);
  }
});

router.post("/verifyemail", async (req, res) => {
  try {
    const tokenData = await Token.findOne({ token: req.body.token });
    if (tokenData) {
      await User.findOneAndUpdate({ _id: tokenData.userid, isVerified: true });

      await Token.findOneAndDelete({ token: req.body.token });
      res.send({ success: true, message: "Email Verified Successlly" });
    } else {
      res.send({ success: false, message: "Invalid token" });
    }
  } catch (error) {
    res.status(500).send(error);
  }
});

Backend => sendEmail.js

const nodemailer = require("nodemailer");
const bcrypt = require("bcrypt");
const Token = require("../models/tokenModel");

module.exports = async (user, mailType) => {
  try {
    const transporter = nodemailer.createTransport({
      service: "gmail",
      host: "smtp.gmail.com",
      port: 587,
      secure: true,
      auth: {
        user: "sh***********th@gmail.com",
        pass: "e**************l",
      },
    });

    const encryptedToken = bcrypt
      .hashSync(user._id.toString(), 10)
      .replaceAll("/", "");
    const token = new Token({
      userid: user._id,
      token: encryptedToken,
    });
    await token.save();
    let mailOptions, emailContent;
    if (mailType === "verifyemail") {
      emailContent = `<div><h1>Please click on the below link to verify your email address</h1> <a href="http://localhost:3000/verifyemail/${encryptedToken}">${encryptedToken}</a>  </div>`;

      mailOptions = {
        from: "sh************th@gmail.com",
        to: user.email,
        subject: "Verify Email For MERN Auth",
        html: emailContent,
      };
    } else {
      emailContent = `<div><h1>Please click on the below link to reset your password</h1> <a href="http://localhost:3000/resetpassword/${encryptedToken}">${encryptedToken}</a>  </div>`;

      mailOptions = {
        from: "shanshangeeth@gmail.com",
        to: user.email,
        subject: "Reset Password",
        html: emailContent,
      };
    }

    await transporter.sendMail(mailOptions);
  } catch (error) {
    console.log(error);
  }
};
// auth: {
//     user: "shanshangeeth@gmail.com",
//     pass: "erwsvgtamrplzssl",
//   },

Backend => authMiddleware.js

const jwt = require("jsonwebtoken");

module.exports = async (req, res, next) => {
  try {
    const token = req.headers["authorization"].split(" ")[1];
    jwt.verify(token, process.env.JWT_SECRET, (err, decoded) => {
      if (err) {
        return res.status(401).send({
          message: "Auth failed",
          success: false,
        });
      } else {
        req.body.userId = decoded.id;

        next();
      }
    });
  } catch (error) {
    return res.status(401).send({
      message: "Auth failed",
      success: false,
    });
  }
};

Backend => tokenmodel.js

const mongoose = require("mongoose");

const tokenSchema = new mongoose.Schema(
  {
    userid: {
      type: String,
      required: true,
    },
    token: {
      type: String,
      required: true,
    },
  },
  { timestamps: true }
);

const tokenModel = mongoose.model("tokens", tokenSchema);

module.exports = tokenModel;

问题分析与修复方案

核心问题

  1. URL特殊字符导致token不匹配:
    使用bcrypt.hashSync生成的验证token可能包含+等URL特殊字符,用户点击链接时浏览器会自动将+转换为空格,导致前端传递的token与数据库中存储的不一致,最终提示"无效token"。首个用户的哈希恰好不含特殊字符,因此验证成功,后续用户则失败。

  2. findOneAndUpdate调用错误:
    verifyemail和resetpassword接口中,User.findOneAndUpdate的参数写法错误,将更新字段放在了查询条件中,导致无法正确更新用户状态(如无法标记邮箱为已验证、无法重置密码)。

  3. 邮件传输配置冲突:
    sendEmail.js中transporter的secure: true与port:587冲突,587端口为TLS端口,需关闭secure选项以启用STARTTLS协议。

修复代码

1. 修复token生成逻辑(sendEmail.js)

改用crypto模块生成无特殊字符的随机token:

const nodemailer = require("nodemailer");
const crypto = require("crypto");
const Token = require("../models/tokenModel");

module.exports = async (user, mailType) => {
  try {
    const transporter = nodemailer.createTransport({
      service: "gmail",
      host: "smtp.gmail.com",
      port: 587,
      secure: false, // 修复端口与secure配置冲突
      auth: {
        user: "sh***********th@gmail.com",
        pass: "e**************l",
      },
    });

    // 生成无特殊字符的随机token
    const encryptedToken = crypto.randomBytes(32).toString('hex');
    const token = new Token({
      userid: user._id,
      token: encryptedToken,
    });
    await token.save();
    let mailOptions, emailContent;
    if (mailType === "verifyemail") {
      emailContent = `<div><h1>请点击下方链接验证你的邮箱地址</h1> <a href="http://localhost:3000/verifyemail/${encryptedToken}">验证邮箱</a>  </div>`;

      mailOptions = {
        from: "sh************th@gmail.com",
        to: user.email,
        subject: "验证你的邮箱(MERN Auth)",
        html: emailContent,
      };
    } else {
      emailContent = `<div><h1>请点击下方链接重置你的密码</h1> <a href="http://localhost:3000/resetpassword/${encryptedToken}">重置密码</a>  </div>`;

      mailOptions = {
        from: "shanshangeeth@gmail.com",
        to: user.email,
        subject: "重置密码",
        html: emailContent,
      };
    }

    await transporter.sendMail(mailOptions);
  } catch (error) {
    console.log(error);
  }
};

2. 修复findOneAndUpdate调用(route.js)

验证邮箱接口修复

router.post("/verifyemail", async (req, res) => {
  try {
    const tokenData = await Token.findOne({ token: req.body.token });
    if (tokenData) {
      // 正确区分查询条件与更新内容
      await User.findOneAndUpdate({ _id: tokenData.userid }, { isVerified: true });
      await Token.findOneAndDelete({ token: req.body.token });
      res.send({ success: true, message: "邮箱验证成功" });
    } else {
      res.send({ success: false, message: "无效的验证链接" });
    }
  } catch (error) {
    res.status(500).send(error);
  }
});

重置密码接口修复

router.post("/resetpassword", async (req, res) => {
  try {
    const tokenData = await Token.findOne({ token: req.body.token });
    if (tokenData) {
      const password = req.body.password;
      const salt = await bcrypt.genSalt(10);
      const hashedPassword = await bcrypt.hash(password, salt);
      // 正确区分查询条件与更新内容
      await User.findOneAndUpdate({ _id: tokenData.userid }, { password: hashedPassword });
      await Token.findOneAndDelete({ token: req.body.token });
      res.send({ success: true, message: "密码重置成功" });
    } else {
      res.send({ success: false, message: "无效的重置链接" });
    }
  } catch (error) {
    res.status(500).send(error);
  }
});

验证修复效果

  1. 重新注册测试用户,检查邮箱中的验证链接
  2. 点击链接后确认邮箱验证状态更新成功
  3. 测试密码重置流程,确认功能正常

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.14 01:20:38