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

如何使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.10 00:25:31