如何在 Better Auth 中构建安全的忘记密码流程并使用 OTP
Source: Dev.to

在我们的上一篇指南中,我们已经使用 Nodemailer 设置了 Better Auth 来处理电子邮件验证和邀请。今天,我们要解决每个生产应用都必不可少的关键功能:“忘记密码”流程。
虽然 Magic Links(直接登录或立即重置密码的链接)很受欢迎,但它们会迫使用户离开你的应用,打开邮件并点击打开新标签页的链接。
有时你希望用户保持在当前页面。本文将构建一个OTP(一次性密码)重置流程。用户输入邮箱后,会收到一个 6 位数的验证码,并在不关闭标签页的情况下重置密码。
后端配置
好消息:如果你已经按照之前的指南操作,你的后端已经准备就绪。
我们之前安装的 emailOTP 插件会自动处理 forget-password 类型。当我们从客户端调用忘记密码功能时,Better Auth 将触发我们在 auth.ts 中定义的 sendVerificationOTP 钩子,并将类型设为 "forget-password"。
只需确保你的 email.ts 模板能够动态处理主题行(或足够通用),以适用于密码重置。
Source: …
客户端实现
我们需要在 UI 中处理两个不同的阶段:
- 请求阶段 – 用户输入邮箱 → 系统发送 OTP。
- 重置阶段 – 用户输入 OTP + 新密码 → 系统更新凭证。
1. 逻辑钩子
为了避免堆砌 200 行 UI 代码,我们先来看使用 authClient 的核心函数。
import { authClient } from "@/lib/auth-client";
import { useState } from "react";
export const usePasswordReset = () => {
const [stage, setStage] = useState("request");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// 步骤 1:发送 OTP
const sendResetOtp = async (email: string) => {
setIsLoading(true);
setError(null);
const { error } = await authClient.forgetPassword.emailOtp({
email,
redirectTo: "/dashboard", // 可选:仅用于流程控制
});
setIsLoading(false);
if (error) {
setError(error.message);
return false;
}
setStage("reset"); // 跳转到下一个 UI 阶段
return true;
};
// 步骤 2:校验 OTP 并设置新密码
const resetPassword = async (
email: string,
otp: string,
password: string
) => {
setIsLoading(true);
setError(null);
const { error } = await authClient.emailOtp.resetPassword({
email,
otp,
password,
});
setIsLoading(false);
if (error) {
setError(error.message);
return false;
}
return true; // 成功!
};
return {
stage,
setStage,
isLoading,
error,
sendResetOtp,
resetPassword,
};
};
2. UI 实现
下面是一个简化的 UI 示例。你可以继续使用 Tailwind 渐变来保持高级感。
请求表单(阶段 1)
if (stage === "request") {
return (
<>
<h2>忘记密码</h2>
<p>输入邮箱以接收验证码。</p>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="name@example.com"
/>
<button
onClick={() => sendResetOtp(email)}
disabled={isLoading || !email}
>
{isLoading ? "发送中…" : "发送 OTP"}
</button>
{error && <p className="error">{error}</p>}
</>
);
}
重置表单(阶段 2)
if (stage === "reset") {
return (
<>
<h2>重置密码</h2>
<p>验证码已发送至 {email}</p>
{/* OTP 输入 – 限制为 6 位数字 */}
<label>OTP 代码</label>
<input
type="text"
value={otp}
onChange={(e) =>
setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))
}
className="tracking-widest text-center text-lg"
placeholder="000000"
/>
<label>新密码</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
<label>确认密码</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<button
onClick={() => {
if (newPassword === confirmPassword) {
resetPassword(email, otp, newPassword);
}
}}
disabled={isLoading}
>
{isLoading ? "重置中…" : "设置新密码"}
</button>
{/* 重发逻辑 */}
<button
onClick={() => sendResetOtp(email)}
className="text-sm underline mt-4"
>
重发验证码
</button>
</>
);
}
Magic Link 与 OTP:该选哪个?
Better Auth 同时支持这两种方式。以下是快速对比,帮助您决定哪种更适合您的应用。
| 功能 | Magic Link | OTP |
|---|---|---|
| 用户体验 | 离开应用,打开邮件 | 保持在应用内 |
| 实现复杂度 | 简单——只需一个链接 | 需要用于输入代码的 UI |
| 安全性 | 一键登录,易受邮件泄露影响 | 代码快速失效,防钓鱼更难 |
| 使用场景 | 快速登录,低摩擦流程 | 密码重置,高安全性场景 |
选择与您产品的用户体验目标和安全需求相匹配的方法。祝编码愉快!
OTP 方法(本指南)
优点
- 无缝上下文: 用户始终停留在你的应用标签页,降低流失率。
- 移动友好: 适用于移动应用,避免处理深度链接(Universal Links)时的错误或复杂设置。
- 感知安全: 用户习惯于使用 2FA 验证码,输入验证码会觉得更安全。
缺点
- 摩擦: 用户必须手动复制粘贴(或输入)验证码。
- 打字错误: 输入错误的验证码或密码需要额外的错误处理。
魔法链接方法
优点
- 零摩擦: 一次点击,用户完成。
- 简洁: 用户无需了解“代码”是什么。
缺点
- 上下文切换: 强迫用户打开新浏览器窗口/标签页。
- 邮件扫描器: 激进的企业邮件扫描器有时会“点击”链接以检查恶意软件,这可能在用户看到之前就使一次性令牌失效。
结论
通过在 Better Auth 中使用 emailOTP 插件,我们创建了一个密码重置流程,使用户能够继续留在我们的应用程序内。虽然这只是一个小小的用户体验细节,但让用户保持在你的应用生态系统中始终能带来更高的转化率和留存率。
欲了解更多信息,请参阅我们的 博客。
