如何实现仅在邮箱验证后使用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
相关产品推荐
相关产品推荐

