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

Express应用中密码重置链接逻辑为何显示undefined?

问题原因

核心是异步代码执行顺序错误:

  • User.findOne()是异步数据库操作,它的回调函数会在查询完成后才触发
  • 你当前代码里,创建 transporter、定义 mailOptions、调用 sendMail 这些逻辑是在User.findOne()之后同步执行的,此时回调里的link还没被赋值,所以邮件模板里的link还是初始的undefined
  • 控制台能打印有效链接,是因为后续异步回调执行时才完成link的赋值和打印,但这时邮件已经发送完毕了
修复方案

方案1:将邮件逻辑移入异步回调内部

把nodemailer相关代码全部放到User.findOne的回调里,确保link赋值完成后再发送邮件:

router.post('/forgot-password', (req, res, next) =>{
    let userEmail = req.body.email;
    // 注意:POST请求参数一般在body里,原代码用req.query.email可能有误,改成req.body.email
    User.findOne({'email': req.body.email}, function(err, user){
        if(err) {
          console.log(err);
          return res.status(500).send("服务器错误");
        }
        if(!user) {
            return res.send("该邮箱未注册");
        }
        // 生成JWT令牌与重置链接
        const secret = JWT_SECRET + user.password;
        const payload = {
            id: user._id, // Mongoose文档的ID字段是_id而非id
            email: userEmail
        };
        let token = jwt.sign(payload, secret, {expiresIn: '15m'});
        // 修复模板字符串写法
        let link = `${process.env.URL}/${user._id}/${token}`;
        console.log(link);

        // 邮件发送逻辑移到此处
        let transporter = nodemailer.createTransport({
            host: 'smtp.gmail.com',
            port: 465,
            secure: true,
            auth: {
                type: 'OAuth2',
                clientId: authClient,
                clientSecret: authCSecret
            }
        });
        
        let mailOptions = {
            from: '你的发送邮箱', // 替换为实际邮箱地址
            to: userEmail,
            replyTo: '你的回复邮箱',
            subject: 'Reset Your Password',
            html: `<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"/><link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"rel="stylesheet"/><link href="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.0.1/mdb.min.css" rel="stylesheet"/><p class="lead">Hey, it looks like you forgot your password.</p><p class="card-text">Follow the link below to reset your password.<div class="text-center"><a class="btn btn-lg btn-primary my-3" href="${link}" target="_blank">click here</a></div></p><script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.0.1/mdb.min.js"></script>`,
            auth: {
                user: authUser,
                refreshToken: authRefreshToken,
                accessToken: authAccessToken,
                expires: 3599
            }
        };
        
        transporter.sendMail(mailOptions, (error, info) => {
            if (error) {
                console.log(error);
                return res.status(500).send("发送邮件失败");
            }
            console.log('Message sent: %s', info.messageId);
            console.log('Preview URL: %s', nodemailer.getTestMessageUrl(info));
            res.send("A link has been sent to your email.");
        });
    });
});

方案2:用Async/Await改写(逻辑更清晰)

将异步操作改为async/await语法,避免回调嵌套,让代码按顺序执行:

router.post('/forgot-password', async (req, res, next) =>{
    try {
        let userEmail = req.body.email;
        // 等待数据库查询结果
        const user = await User.findOne({'email': userEmail});
        if(!user) {
            return res.send("该邮箱未注册");
        }
        // 生成令牌与链接
        const secret = JWT_SECRET + user.password;
        const payload = {
            id: user._id,
            email: userEmail
        };
        let token = jwt.sign(payload, secret, {expiresIn: '15m'});
        let link = `${process.env.URL}/${user._id}/${token}`;
        console.log(link);

        // 创建邮件 transporter
        let transporter = nodemailer.createTransport({
            host: 'smtp.gmail.com',
            port: 465,
            secure: true,
            auth: {
                type: 'OAuth2',
                clientId: authClient,
                clientSecret: authCSecret
            }
        });
        
        let mailOptions = {
            from: '你的发送邮箱',
            to: userEmail,
            replyTo: '你的回复邮箱',
            subject: 'Reset Your Password',
            html: `<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"/><link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"rel="stylesheet"/><link href="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.0.1/mdb.min.css" rel="stylesheet"/><p class="lead">Hey, it looks like you forgot your password.</p><p class="card-text">Follow the link below to reset your password.<div class="text-center"><a class="btn btn-lg btn-primary my-3" href="${link}" target="_blank">click here</a></div></p><script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mdb-ui-kit/6.0.1/mdb.min.js"></script>`,
            auth: {
                user: authUser,
                refreshToken: authRefreshToken,
                accessToken: authAccessToken,
                expires: 3599
            }
        };
        
        // 等待邮件发送完成
        await transporter.sendMail(mailOptions);
        console.log('Message sent successfully');
        res.send("A link has been sent to your email.");
    } catch(err) {
        console.log(err);
        res.status(500).send("服务器错误");
    }
});
额外注意事项
  • 原代码中User.findOne的查询条件用了req.query.email,但POST请求的参数通常在req.body里,需确认前端参数传递方式
  • Mongoose文档的ID字段默认是_id,而非id,拼接链接时要注意字段名
  • URL拼接的原写法process.env.URL/${username.id}/${token}是错误的模板字符串语法,需改为${process.env.URL}/${user._id}/${token}`

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.08 18:30:17