- 后端
- 前端
【免费下载链接】rallly
Rallly is an open-source scheduling and collaboration tool designed to make organizing events and meetings easier.
导读
本文以 Rallly 开源仓库中的.agents/skills/email-and-password-best-practices/SKILL.md为骨架,完整讲解基于 Better Auth 实现邮件与密码认证的关键实践:邮箱验证(Email Verification)、密码重置(Password Reset)、回调 URL、客户端校验与密码哈希。Rallly 的认证实现位于 apps/web/src/lib/auth.ts 与 apps/web/src/lib/auth-client.ts,本文将以这份真实的 Better Auth 配置为主线,逐项展开配置参数、安全原理与完整可运行的代码示例。读者读完可掌握一套可直接落地的邮箱密码认证安全基线。
一、邮箱验证:从防滥用到强制校验
1.1 为什么需要邮箱验证
启用邮箱验证的核心目的有两个:防止虚假注册(fake sign-ups)以及确保用户拥有所注册的邮箱。当攻击者无法控制验证邮件到达的邮箱时,垃圾账号、一次性邮箱滥用的门槛会显著提高。Rallly 在 apps/web/src/lib/auth.ts 中启用emailVerification,并在 apps/web/src/features/auth/utils.ts 提供了isEmailBlocked与isTemporaryEmail两个辅助函数,分别配合ALLOWED_EMAILS环境变量实现域名白名单,以及内置临时邮箱域名列表过滤一次性邮箱,这正是对"防止虚假注册"这一目标的仓库级印证。
1.2 配置发送验证邮件
Better Auth 通过emailVerification.sendVerificationEmail让开发者接管验证邮件的发送。该回调接收{ user, url, token }与request,其中url是完整的验证链接,token可在需要自定义验证 URL 时使用:
import { betterAuth } from "better-auth"; import { sendEmail } from "./email"; // your email sending function export const auth = betterAuth({ emailVerification: { sendVerificationEmail: async ({ user, url, token }, request) => { await sendEmail({ to: user.email, subject: "Verify your email address", text: `Click the link to verify your email: ${url}`, }); }, }, });注意:url参数包含完整的验证链接;token仅在需要自行拼接自定义验证 URL 时使用。
1.3 强制邮箱验证:未验证禁止登录
更严格的安全策略是启用emailAndPassword.requireEmailVerification,在用户验证邮箱之前阻止其登录;启用后,每次未验证用户尝试登录时都会收到新的验证邮件:
export const auth = betterAuth({ emailAndPassword: { requireEmailVerification: true, }, });注意:该选项要求必须配置sendVerificationEmail,且仅作用于 email/password 方式的登录。
Rallly 实际将requireEmailVerification设为true(见 apps/web/src/lib/auth.ts),并且在 OTP 邮件验证插件上启用overrideDefaultEmailVerification: true与autoSignInAfterVerification: true,把登录与注册统一收敛到同一条验证流程(见 apps/web/src/lib/auth.ts),进一步说明"验证后自动登录"的落地方式。
二、客户端校验:体验与负载的双重收益
Better Auth 在服务端校验输入,但客户端校验仍然被强烈推荐,理由有二:
- 改善用户体验(Improved UX):用户能在输入不符合要求时立即获得反馈,而不是等待一次服务端往返。
- 降低服务器负载(Reduced server load):无效请求被尽早拦截,减少发往认证服务器的无谓网络流量。
Rallly 在客户端以 react-hook-form + zod 做了同样的校验:ForgotPasswordForm用z.email(...)校验邮箱格式(apps/web/src/app/[locale]/(auth)/forgot-password/components/forgot-password-form.tsx);ResetPasswordForm用usePasswordValidationSchema()校验新密码(apps/web/src/app/[locale]/(auth)/reset-password/components/reset-password-form.tsx)。此外 Rallly 还提供了PasswordStrengthMeter强度指示条(apps/web/src/features/auth/components/password-strength-meter.tsx),借助 zxcvbn 计算 0~4 分强度并实时展示(apps/web/src/features/auth/utils.ts),要求强度达到good(3 分)才允许提交(apps/web/src/features/auth/schema.ts)。
三、回调 URL:始终使用带源站的绝对地址
在注册与登录请求中,回调 URL必须使用包含 origin 的绝对 URL。这避免 Better Auth 自行推断源站(origin),而前后端分属不同域名时,推断很容易出错:
const { data, error } = await authClient.signUp.email({ callbackURL: "https://example.com/callback", // absolute URL with origin });Rallly 的思路与此一脉相承:它在 apps/web/src/lib/auth.ts 用absoluteUrl("/api/better-auth")生成baseURL,在 apps/web/src/lib/auth-client.ts 中客户端同样使用absoluteUrl("/api/better-auth")作为baseURL,并配置trustedOrigins: [absoluteUrl()](apps/web/src/lib/auth.ts),把信任的回调来源显式锁定为自身源站,从根源上规避开放重定向。
四、密码重置流程:从回调到安全细节
4.1 提供sendResetPassword函数
要允许用户重置密码,首先需要为 email/password 认证器提供sendResetPassword函数:
import { betterAuth } from "better-auth"; import { sendEmail } from "./email"; // your email sending function export const auth = betterAuth({ emailAndPassword: { enabled: true, // Custom email sending function to send reset-password email sendResetPassword: async ({ user, url, token }, request) => { void sendEmail({ to: user.email, subject: "Reset your password", text: `Click the link to reset your password: ${url}`, }); }, // Optional event hook onPasswordReset: async ({ user }, request) => { // your logic here console.log(`Password for user ${user.email} has been reset.`); }, }, });Rallly 的落地实现会进一步考虑国际化与品牌化:sendResetPassword中按用户邮箱查语言偏好(getUserLocaleByEmail),再调用sendResetPasswordEmail模板发送含resetLink的邮件(apps/web/src/lib/auth.ts);邮件模板位于 packages/emails/src/templates/reset-password.tsx,导出sendResetPasswordEmail函数。onPasswordReset钩子在 apps/web/src/lib/auth.ts 中被用作埋点事件password_reset的触发点。
4.2 时序攻击防护(Timing attack prevention)
Better Auth 在密码重置流程内置了多项时序攻击防护:
- 后台发送邮件(Background email sending):内部通过
runInBackgroundOrAwait发送重置邮件,不阻塞响应,使攻击者无法通过测量响应时间差来判断邮箱是否存在。 - 无效请求的虚拟操作(Dummy operations on invalid requests):当用户不存在时,Better Auth 仍会执行令牌生成和一次数据库查询(使用虚拟值),保持响应时间一致。
- 恒定响应消息(Constant response message):无论用户是否存在,API 恒定返回
"If this email exists in our system, check your email for the reset link"。
在 Serverless 平台上,需要配置后台任务处理器(background task handler)确保邮件可靠发送:
export const auth = betterAuth({ advanced: { backgroundTasks: { handler: (promise) => { // Use platform-specific methods like waitUntil waitUntil(promise); }, }, }, });Rallly 的ForgotPasswordForm提交后无论结果如何都统一展示"Check your email / If an account exists with this email address, we've sent a password reset link to your inbox"文案(apps/web/src/app/[locale]/(auth)/forgot-password/components/forgot-password-form.tsx),与"恒定响应消息"的策略完全一致,是时序攻击防护在前端交互层的呼应。
4.3 令牌安全(Token security)
- 密码学随机令牌:重置令牌使用
generateId(24)生成,为 24 位字母数字字符串(a-z、A-Z、0-9),熵值高。 - 令牌过期:默认1 小时后过期,可用
resetPasswordTokenExpiresIn(单位:秒)配置:
export const auth = betterAuth({ emailAndPassword: { enabled: true, resetPasswordTokenExpiresIn: 60 * 30, // 30 minutes }, });- 一次性令牌:密码重置成功后令牌立即被删除,防止重复使用。
Rallly 在 apps/web/src/app/[locale]/(auth)/reset-password/components/reset-password-form.tsx 中校验 URL 上的token参数,缺失或过期时直接展示"The password reset link is invalid or has expired. Please request a new one."的错误提示,对应令牌过期与失效的前端处理。
4.4 会话吊销(Session revocation)
启用revokeSessionsOnPasswordReset可在密码被重置时吊销所有既有会话,确保攻击者即使持有有效会话也会被立即终止:
export const auth = betterAuth({ emailAndPassword: { enabled: true, revokeSessionsOnPasswordReset: true, }, });4.5 重定向 URL 校验(Redirect URL validation)
redirectTo参数会与trustedOrigins配置进行校验,防止开放重定向攻击(open redirect attacks)。恶意重定向 URL 将被拒绝并返回 403 错误。Rallly 在重置密码成功后调用validateRedirectUrl再决定跳转目标(apps/web/src/app/[locale]/(auth)/reset-password/components/reset-password-form.tsx),是这一安全点在客户端侧的二次保障。
4.6 密码长度要求(Password requirements)
重置密码时,新密码必须满足长度要求:
- 最小长度:默认 8 字符,可用
minPasswordLength配置 - 最大长度:默认 128 字符,可用
maxPasswordLength配置
export const auth = betterAuth({ emailAndPassword: { enabled: true, minPasswordLength: 12, maxPasswordLength: 256, }, });Rallly 在客户端用.max(128)与服务端保持一致(apps/web/src/features/auth/schema.ts),并且额外通过 zxcvbn 强度评分要求密码强度达到good级别——即在长度限制之上再叠加一层"猜测难度"约束。
4.7 发起密码重置请求
配置完成后,即可调用requestPasswordReset向用户发送重置链接;若用户存在,将触发你配置的sendResetPassword函数:
const data = await auth.api.requestPasswordReset({ body: { email: "john.doe@example.com", // required redirectTo: "https://example.com/reset-password", }, });或使用 authClient:
const { data, error } = await authClient.requestPasswordReset({ email: "john.doe@example.com", // required redirectTo: "https://example.com/reset-password", });注意:email为必填;同时建议配置redirectTo以获得更顺畅的用户体验。
Rallly 的 Forgot Password 页面正是按此模式实现:提交邮箱后调用authClient.requestPasswordReset({ email, redirectTo: "/reset-password" })(apps/web/src/app/[locale]/(auth)/forgot-password/components/forgot-password-form.tsx),随后由 apps/web/src/app/[locale]/(auth)/reset-password/page.tsx 承载新密码表单页,该页面在EMAIL_LOGIN_ENABLED === "false"时直接返回 404(见同文件第 18~20 行),避免禁用邮件登录时残留入口。
五、密码哈希:默认 scrypt 与自定义算法
Better Auth 默认使用scrypt进行密码哈希,理由充分:
- 设计上速度慢且内存密集,使暴力破解成本高昂;
- Node.js 原生支持,无需外部依赖;
- 在 Argon2id 不可用时,OWASP 推荐使用 scrypt。
5.1 切换自定义哈希算法(如 Argon2id)
若需改用其他算法,可在emailAndPassword.password配置中提供自定义hash与verify函数:
import { betterAuth } from "better-auth"; import { hash, verify, type Options } from "@node-rs/argon2"; const argon2Options: Options = { memoryCost: 65536, // 64 MiB timeCost: 3, // 3 iterations parallelism: 4, // 4 parallel lanes outputLen: 32, // 32 byte output algorithm: 2, // Argon2id variant }; export const auth = betterAuth({ emailAndPassword: { enabled: true, password: { hash: (password) => hash(password, argon2Options), verify: ({ password, hash: storedHash }) => verify(storedHash, password, argon2Options), }, }, });注意:如果在既有系统上切换哈希算法,旧算法哈希的用户将无法登录,需要提前规划迁移策略。
六、在 Rallly 中的完整实践印证
Rallly 的 apps/web/src/lib/auth.ts 是这份技能文档在真实项目中的完整落地范本,可逐项对照:
emailVerification.autoSignInAfterVerification: true,配合 OTP 插件统一登录/注册验证流程(第 186~188、224~236 行);emailAndPassword.requireEmailVerification: true,强制邮箱验证后才可登录(第 164 行);emailAndPassword.disableSignUp: true,账号只通过已验证的 OTP 创建、密码只能从已登录会话中设置,避免他人预占邮箱(第 165~169 行);sendResetPassword与onPasswordReset分别接管重置邮件发送与事后埋点(第 170~184 行);trustedOrigins: [absoluteUrl()]与baseURL锁定回调与跳转源站(第 62、699~700 行);- 会话层还配置了 60 天有效期、1 天滑动续期与 cookie 缓存(第 683~690 行),相关常量定义在 apps/web/src/lib/auth-config.ts。
这些配置与本文前述的每条最佳实践一一对应,可作为集成 Better Auth 邮箱密码认证时可直接参考的实现蓝本。客户端侧的完整调用链(ForgotPasswordForm→authClient.requestPasswordReset→ResetPasswordForm→authClient.resetPassword)可从 apps/web/src/app/[locale]/(auth)/forgot-password/components/forgot-password-form.tsx 与 apps/web/src/app/[locale]/(auth)/reset-password/components/reset-password-form.tsx 中完整追溯。
结语
邮箱验证、客户端校验、绝对回调 URL、带多重安全措施(时序攻击防护、一次性令牌、会话吊销、重定向校验)的密码重置,以及 scrypt/Argon2id 密码哈希,共同构成了 Better Auth 邮件密码认证的安全基线。这些配置可直接照搬于任何基于 Better Auth 的 Node.js 应用,而在 Rallly 仓库中,它们已经过生产级打磨并配有完整的前后端实现可供研读参考。
- 后端
- 前端
【免费下载链接】rallly
Rallly is an open-source scheduling and collaboration tool designed to make organizing events and meetings easier.
相关推荐
Wasp 邮箱认证(Email Auth)完整指南:邮箱验证、密码重置与配置 API 详解(v0.16)
Wasp 邮箱认证(Email Auth)完整指南:邮箱验证、密码重置与配置 API 详解(v0.16) 本文面向使用 Wasp v0.16 构建全栈应用的开发
后端前端Web框架编译器开发工具代码生成Wasp 邮件认证完全指南:邮箱登录、验证与密码重置的声明式配置实战
Wasp 邮件认证完全指南:邮箱登录、验证与密码重置的声明式配置实战 本指南以 Wasp 0.18 官方文档( email.md https://link.gi
后端前端Web框架编译器开发工具代码生成Wasp 邮箱认证完整实战指南:从注册登录到邮件验证与密码重置
Wasp 邮箱认证完整实战指南:从注册登录到邮件验证与密码重置 本指南以 Wasp 全栈框架(当前仓库 waspc https://link.gitcode.c
后端前端Web框架编译器开发工具代码生成
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考