如何使用NextAuth Credential Provider结合MongoDB实现密码重置
使用NextAuth Credential Provider结合MongoDB实现密码重置
步骤1:更新用户Schema
需要添加密码重置相关字段来存储重置token和过期时间,修改你的userSchema:
const userSchema = new mongoose.Schema<UserSchemaType>( { name: { type: String, required: true }, email: { type: String, required: true, unique: true }, password: { type: String, required: true }, img: { type: String, required: true }, isAdmin: { type: Boolean, required: true, default: false }, // 新增密码重置字段 resetPasswordToken: String, resetPasswordExpire: Date, }, { timestamps: true, } );
步骤2:添加重置Token相关模型方法
在User模型中添加静态方法,用于生成、哈希和验证重置token:
// 生成重置token并哈希后存入数据库 userSchema.statics.generateResetToken = async function(email) { const user = await this.findOne({ email }); if (!user) throw new Error("用户不存在"); // 生成随机原始token const resetToken = require('crypto').randomBytes(32).toString('hex'); // 哈希token后存储(避免明文泄露风险) user.resetPasswordToken = require('crypto').createHash('sha256').update(resetToken).digest('hex'); // 设置1小时过期时间 user.resetPasswordExpire = Date.now() + 3600000; await user.save({ validateBeforeSave: false }); return resetToken; }; // 根据原始token查找有效用户 userSchema.statics.findValidResetUser = async function(token) { const hashedToken = require('crypto').createHash('sha256').update(token).digest('hex'); return this.findOne({ resetPasswordToken: hashedToken, resetPasswordExpire: { $gt: Date.now() } }); };
步骤3:实现邮件发送工具
安装nodemailer依赖:npm install nodemailer,创建utils/sendEmail.js:
const nodemailer = require('nodemailer'); const sendResetEmail = async(email, resetToken) => { // 替换为你的邮箱服务商配置(示例用通用SMTP配置) const transporter = nodemailer.createTransport({ host: "smtp.your-email-provider.com", port: 587, secure: false, auth: { user: "your-account@example.com", pass: "your-email-password", }, }); const resetUrl = `${process.env.NEXTAUTH_URL}/reset-password?token=${resetToken}`; const mailOptions = { from: '"你的应用名称" <your-account@example.com>', to: email, subject: "密码重置请求", html: ` <p>你发起了密码重置请求,请点击下方链接设置新密码:</p> <a href="${resetUrl}">${resetUrl}</a> <p>链接1小时内有效,若非本人操作请忽略此邮件。</p> `, }; await transporter.sendMail(mailOptions); }; module.exports = sendResetEmail;
步骤4:创建API接口
4.1 忘记密码请求接口(/pages/api/auth/forgot-password.js)
import db from '../../../utils/db'; import User from '../../../models/User'; import sendResetEmail from '../../../utils/sendEmail'; export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: '不允许的请求方法' }); } try { await db.connect(); const { email } = req.body; const resetToken = await User.generateResetToken(email); await sendResetEmail(email, resetToken); res.status(200).json({ message: '重置邮件已发送,请查收邮箱' }); } catch (error) { res.status(400).json({ message: error.message }); } finally { await db.disconnect(); } }
4.2 重置密码提交接口(/pages/api/auth/reset-password.js)
import db from '../../../utils/db'; import User from '../../../models/User'; import bcryptjs from 'bcryptjs'; export default async function handler(req, res) { if (req.method !== 'POST') { return res.status(405).json({ message: '不允许的请求方法' }); } try { await db.connect(); const { token, password } = req.body; const user = await User.findValidResetUser(token); if (!user) throw new Error('重置链接无效或已过期'); // 加密新密码(和登录逻辑保持一致) user.password = bcryptjs.hashSync(password, 10); // 清空重置相关字段 user.resetPasswordToken = undefined; user.resetPasswordExpire = undefined; await user.save(); res.status(200).json({ message: '密码重置成功,请重新登录' }); } catch (error) { res.status(400).json({ message: error.message }); } finally { await db.disconnect(); } }
步骤5:前端页面实现
5.1 忘记密码页面(/pages/forgot-password.js)
import { useState } from 'react'; export default function ForgotPassword() { const [email, setEmail] = useState(''); const [message, setMessage] = useState(''); const [error, setError] = useState(''); const handleSubmit = async(e) => { e.preventDefault(); setMessage(''); setError(''); try { const res = await fetch('/api/auth/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }) }); const data = await res.json(); if (!res.ok) throw new Error(data.message); setMessage(data.message); } catch (err) { setError(err.message); } }; return ( <div className="max-w-md mx-auto mt-10"> <h1 className="text-2xl font-bold mb-4">忘记密码</h1> {error && <p className="text-red-500 mb-4">{error}</p>} {message && <p className="text-green-500 mb-4">{message}</p>} <form onSubmit={handleSubmit}> <div className="mb-4"> <label className="block mb-2" htmlFor="email">邮箱</label> <input type="email" id="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="w-full px-4 py-2 border rounded" /> </div> <button type="submit" className="w-full bg-blue-500 text-white py-2 rounded">发送重置链接</button> </form> </div> ); }
5.2 重置密码页面(/pages/reset-password.js)
import { useState, useEffect } from 'react'; import { useRouter } from 'next/router'; export default function ResetPassword() { const router = useRouter(); const [token, setToken] = useState(''); const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [message, setMessage] = useState(''); const [error, setError] = useState(''); useEffect(() => { if (router.query.token) setToken(router.query.token); }, [router.query]); const handleSubmit = async(e) => { e.preventDefault(); setMessage(''); setError(''); if (password !== confirmPassword) { setError('两次输入的密码不一致'); return; } try { const res = await fetch('/api/auth/reset-password', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, password }) }); const data = await res.json(); if (!res.ok) throw new Error(data.message); setMessage(data.message); setTimeout(() => router.push('/login'), 2000); } catch (err) { setError(err.message); } }; return ( <div className="max-w-md mx-auto mt-10"> <h1 className="text-2xl font-bold mb-4">重置密码</h1> {error && <p className="text-red-500 mb-4">{error}</p>} {message && <p className="text-green-500 mb-4">{message}</p>} <form onSubmit={handleSubmit}> <div className="mb-4"> <label className="block mb-2" htmlFor="password">新密码</label> <input type="password" id="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="w-full px-4 py-2 border rounded" /> </div> <div className="mb-4"> <label className="block mb-2" htmlFor="confirmPassword">确认新密码</label> <input type="password" id="confirmPassword" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} required className="w-full px-4 py-2 border rounded" /> </div> <button type="submit" className="w-full bg-blue-500 text-white py-2 rounded">重置密码</button> </form> </div> ); }
注意事项
- 替换邮件服务配置为你实际使用的邮箱服务商信息(如Gmail、阿里云邮箱等),部分服务商需要开启SMTP权限或应用密码。
- 确保环境变量
NEXTAUTH_URL正确配置(开发环境为http://localhost:3000)。 - 可根据需求添加密码强度验证(如长度、特殊字符要求)。
- 在登录页面添加“忘记密码”链接,跳转至
/forgot-password页面。
内容的提问来源于stack exchange,提问作者Ree
相关产品推荐
相关产品推荐

