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;
问题分析与修复方案
核心问题
URL特殊字符导致token不匹配:
使用bcrypt.hashSync生成的验证token可能包含+等URL特殊字符,用户点击链接时浏览器会自动将+转换为空格,导致前端传递的token与数据库中存储的不一致,最终提示"无效token"。首个用户的哈希恰好不含特殊字符,因此验证成功,后续用户则失败。findOneAndUpdate调用错误:verifyemail和resetpassword接口中,User.findOneAndUpdate的参数写法错误,将更新字段放在了查询条件中,导致无法正确更新用户状态(如无法标记邮箱为已验证、无法重置密码)。邮件传输配置冲突:
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); } });
验证修复效果
- 重新注册测试用户,检查邮箱中的验证链接
- 点击链接后确认邮箱验证状态更新成功
- 测试密码重置流程,确认功能正常
内容的提问来源于stack exchange,提问作者SHANGEETH SS
相关产品推荐
相关产品推荐

