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

如何实现仅在邮箱验证后使用Passport.js认证新用户?

解决方案:解耦用户注册与Passport认证,实现邮箱验证后激活登录

核心思路很明确:把用户文档创建和Passport认证权限拆成两个独立步骤——注册时只生成未验证的用户记录,不直接开放登录权限;用户完成邮箱验证后,再标记用户为已激活,此时才能通过Passport正常登录。

下面是具体的分步实现:

1. 修改用户Schema,添加验证状态字段

首先给你的Profile模型新增几个字段,用于跟踪验证状态、验证token和过期时间:

const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');

const profileSchema = new mongoose.Schema({
  email: { type: String, required: true, unique: true },
  // 其他用户信息字段(比如昵称、个人资料等)
  isVerified: { type: Boolean, default: false }, // 标记是否已验证邮箱
  verificationToken: String, // 邮箱验证token
  tokenExpires: Date // token过期时间
});

// 配置passport-local-mongoose,指定用email作为username字段
profileSchema.plugin(passportLocalMongoose, { usernameField: 'email' });

module.exports = mongoose.model('Profile', profileSchema);

2. 重构注册流程:手动创建未验证用户

不再直接使用Profile.register()(这个方法会默认创建可登录的用户),而是手动实例化用户、设置密码、生成验证token,最后保存并发送验证邮件:

const crypto = require('crypto');
const Profile = require('../models/Profile');
const nodemailer = require('nodemailer'); // 假设你用nodemailer发邮件

// 注册接口路由
app.post('/register', async (req, res) => {
  try {
    // 先检查邮箱是否已被注册
    const existingUser = await Profile.findOne({ email: req.body.email });
    if (existingUser) {
      return res.status(400).json({ message: '该邮箱已被注册' });
    }

    // 1. 创建未验证的用户实例
    const user = new Profile({
      email: req.body.email,
      // 填充其他用户字段(从req.body获取)
    });

    // 2. 生成随机验证token和过期时间(比如1小时有效期)
    user.verificationToken = crypto.randomBytes(32).toString('hex');
    user.tokenExpires = Date.now() + 3600000; // 1小时后过期

    // 3. 使用passport-local-mongoose提供的setPassword方法设置密码
    await user.setPassword(req.body.password);

    // 4. 保存用户到数据库
    await user.save();

    // 5. 发送验证邮件
    const verificationUrl = `http://your-domain.com/verify/${user._id}/${user.verificationToken}`;
    const transporter = nodemailer.createTransport({
      // 你的邮件服务商配置(比如Gmail、SendGrid等)
    });

    await transporter.sendMail({
      to: user.email,
      subject: '请验证你的邮箱',
      html: `<p>点击下方链接完成邮箱验证:</p><a href="${verificationUrl}">验证链接</a>`
    });

    res.status(200).json({ message: '注册成功,请检查邮箱完成验证' });
  } catch (err) {
    res.status(500).json({ message: '注册失败:' + err.message });
  }
});

3. 实现邮箱验证路由,激活用户登录权限

当用户点击验证链接时,校验token的有效性和过期时间,通过后标记用户为已验证:

app.get('/verify/:userId/:token', async (req, res) => {
  try {
    const { userId, token } = req.params;

    // 查找匹配的用户,同时检查token未过期
    const user = await Profile.findOne({
      _id: userId,
      verificationToken: token,
      tokenExpires: { $gt: Date.now() }
    });

    if (!user) {
      return res.status(400).send('无效或已过期的验证链接');
    }

    // 标记用户为已验证,清除验证相关字段
    user.isVerified = true;
    user.verificationToken = undefined;
    user.tokenExpires = undefined;
    await user.save();

    // 这里可以自动登录用户,或者跳转到登录页面
    res.send('邮箱验证成功!现在可以登录你的账户了');
  } catch (err) {
    res.status(500).send('验证失败:' + err.message);
  }
});

4. 调整Passport Local策略,添加验证状态检查

最后修改Passport的认证逻辑,确保只有已验证邮箱的用户才能登录:

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const Profile = require('../models/Profile');

passport.use(new LocalStrategy({
  usernameField: 'email' // 继续用邮箱作为登录用户名
}, async (email, password, done) => {
  try {
    const user = await Profile.findOne({ email });
    
    if (!user) {
      return done(null, false, { message: '该邮箱未注册' });
    }

    // 关键:检查用户是否已验证邮箱
    if (!user.isVerified) {
      return done(null, false, { message: '请先验证你的邮箱' });
    }

    // 使用passport-local-mongoose的authenticate方法验证密码
    const isPasswordValid = await user.authenticate(password);
    if (!isPasswordValid) {
      return done(null, false, { message: '密码错误' });
    }

    return done(null, user);
  } catch (err) {
    return done(err);
  }
}));

替代策略参考

如果不想手动处理这么多细节,也可以考虑:

  • 使用passport-local-mongoose的findByUsername钩子,在认证前拦截检查用户的验证状态
  • 集成第三方邮箱验证服务,但本质逻辑和上面一致

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.14 07:46:37