电子邮件与密码

使用 Better Auth 实现电子邮件和密码认证。

电子邮件和密码认证是许多应用常用的一种方法。Better Auth 提供了内置的电子邮件和密码认证器,您可以轻松集成到项目中。

如果您更喜欢基于用户名的认证,请查看 用户名插件。它扩展了 电子邮件和密码认证器,支持用户名。

启用电子邮件和密码

要启用电子邮件和密码认证,您需要在 auth 配置的 emailAndPassword.enabled 选项中设置为 true

auth.ts
import { betterAuth } from "better-auth";

export const auth = betterAuth({
  emailAndPassword: { 
    enabled: true, 
  }, 
});

如果未启用,则不允许您使用电子邮件和密码进行登录或注册。

使用方法

注册

要注册用户,可以使用客户端提供的 signUp.email 函数。

POST/sign-up/email
const { data, error } = await authClient.signUp.email({    name: "John Doe", // required    email: "john.doe@example.com", // required    password: "password1234", // required    image: "https://example.com/image.png",    callbackURL: "https://example.com/callback",});
Parameters
namestringrequired

用户名称。

emailstringrequired

用户的电子邮件地址。

passwordstringrequired

用户密码,默认应至少 8 个字符且最多 128 个字符。

imagestring

可选的用户头像图片。

callbackURLstring

用户注册后可选的重定向 URL。

以上是注册邮箱端点的默认属性, 但通过额外字段或特殊插件,您可能会传递更多属性给端点。

登录

要让用户登录,可以使用客户端提供的 signIn.email 函数。

POST/sign-in/email
const { data, error } = await authClient.signIn.email({    email: "john.doe@example.com", // required    password: "password1234", // required    rememberMe: true,    callbackURL: "https://example.com/callback",});
Parameters
emailstringrequired

用户电子邮件地址。

passwordstringrequired

用户密码,默认应至少 8 个字符且最多 128 个字符。

rememberMeboolean

如果为 false,用户关闭浏览器时将登出。(可选)(默认:true)

callbackURLstring

用户登录后可选的重定向 URL。(可选)

以上是登录邮箱端点的默认属性,但通过额外字段或特殊插件,您可以传递不同属性给端点。

登出

要让用户登出,可以使用客户端提供的 signOut 函数。

POST/sign-out
await authClient.signOut();

您可以传递 fetchOptions 来在成功后进行重定向

import { authClient } from "@/lib/auth-client"

await authClient.signOut({
  fetchOptions: {
    onSuccess: () => {
      router.push("/login"); // 重定向到登录页面
    },
  },
});

邮箱验证

要启用邮箱验证,您需要传递一个发送验证邮件的函数,该函数发送包含验证链接的邮件。sendVerificationEmail 函数接收一个包含以下属性的数据对象:

  • user:用户对象。
  • url:发送给用户的包含令牌的 URL。
  • token:用于完成邮箱验证的验证令牌。

以及作为第二个参数的 request 对象。

auth.ts
import { betterAuth } from "better-auth";
import { sendEmail } from "./email"; // 您的发送邮件函数

export const auth = betterAuth({
  emailVerification: {
    sendVerificationEmail: async ( { user, url, token }, request) => {
      void sendEmail({
        to: user.email,
        subject: "请验证您的电子邮件地址",
        text: `点击链接验证您的电子邮件:${url}`,
      });
    },
  },
});

避免等待邮件发送以防止时序攻击。在无服务器平台上,请使用 waitUntil 或类似方法确保邮件发送成功。

客户端可以调用 sendVerificationEmail 函数给用户发送验证链接。这将触发您在 auth 配置中提供的 sendVerificationEmail 函数。

当用户点击邮件中的链接时,如果令牌有效,将重定向到 callbackURL 参数中提供的 URL;如果令牌无效,则会重定向到带有错误消息的 callbackURL(查询字符串为 ?error=invalid_token)。

需要邮箱验证

如果启用此功能,用户必须验证邮箱后才能登录。每当用户尝试登录时,将调用 sendVerificationEmail

该功能仅在您实现了 sendVerificationEmail 并且用户 以电子邮件和密码方式登录时生效。

开启 requireEmailVerification 时,使用已存在的邮箱注册会返回成功响应,而非错误,以防止用户枚举。

auth.ts
export const auth = betterAuth({
  emailAndPassword: {
    requireEmailVerification: true, 
  },
});

您可以用 onExistingUserSignUp 回调通知已有用户有人尝试用他们的邮箱注册:

auth.ts
import { betterAuth } from "better-auth";
import { sendEmail } from "./email"; // 您的发送邮件函数

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    onExistingUserSignUp: async ({ user }, request) => {
      void sendEmail({
        to: user.email,
        subject: "有人尝试用您的邮箱注册",
        text: "有人试图使用您的邮箱地址创建账号。如果是您,请尝试登录;如果不是,可以忽略这封邮件。",
      });
    },
  },
});

如果用户尝试在未验证邮箱时登录,您可以捕获错误并给用户显示提示。

import { authClient } from "@/lib/auth-client"

await authClient.signIn.email(
  {
    email: "email@example.com",
    password: "password",
  },
  {
    onError: (ctx) => {
      // 处理错误
      if (ctx.error.status === 403) {
        alert("请先验证您的电子邮件地址");
      }
      // 也可以显示原始错误信息
      alert(ctx.error.message);
    },
  }
);

邮箱枚举保护

当启用 requireEmailVerification 或将 autoSignIn 设为 false 时,注册端点会通过统一返回 200 响应,来防止邮箱枚举,无论邮箱是否已注册。此做法符合 OWASP 认证最佳实践

这一保护只在注册响应不包含 session 令牌时生效——即 requireEmailVerificationtrueautoSignInfalse。默认配置下,已存在邮箱仍会返回 422 错误。

类似地,/change-email 端点不再透露目标邮箱是否已注册 —— 总是返回成功响应。

为用户字段添加插件

如果您使用了扩展用户表字段的插件(例如 admintwo-factorphone-number),则模拟响应需要包含这些字段,以使其与真实注册无法区分。可使用 customSyntheticUser 选项构建完整用户对象:

auth.ts
import { betterAuth } from "better-auth";
import { admin } from "better-auth/plugins";

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    requireEmailVerification: true,
    customSyntheticUser: ({ coreFields, additionalFields, id }) => ({
      ...coreFields,
      // Admin 插件字段(按数据库字段顺序)
      role: "user",
      banned: false,
      banReason: null,
      banExpires: null,
      // 您的额外字段
      ...additionalFields,
      // ID 必须放后以匹配数据库输出顺序
      id,
    }),
  },
  plugins: [admin()],
});

回调接收三个构建块:

  • coreFieldsnameemailemailVerifiedimagecreatedAtupdatedAt
  • additionalFields — 含默认值的 user.additionalFields
  • id — 生成的用户 ID,符合您的 ID 策略

您应依数据库字段顺序组装:核心字段 → 插件字段 → 额外字段 → ID。每个插件会说明所需字段 — 请参考 admin 插件 示例。

手动触发邮件验证

您可以调用 sendVerificationEmail 函数手动触发邮箱验证。

import { authClient } from "@/lib/auth-client"

await authClient.sendVerificationEmail({
  email: "user@email.com",
  callbackURL: "/", // 验证后的重定向 URL
});

请求密码重置

允许用户重置密码前,您需要为电子邮件和密码认证器提供 sendResetPassword 函数。该函数接收一个包含以下属性的数据对象:

  • user:用户对象。
  • url:发送给用户的包含令牌的 URL。
  • token:用于完成密码重置的验证令牌。

及作为第二参数的 request 对象。

auth.ts
import { betterAuth } from "better-auth";
import { sendEmail } from "./email"; // 您的发送邮件函数

export const auth = betterAuth({
  emailAndPassword: {
    enabled: true,
    sendResetPassword: async ({user, url, token}, request) => {
      void sendEmail({
        to: user.email,
        subject: "重置您的密码",
        text: `点击链接重置您的密码:${url}`,
      });
    },
    onPasswordReset: async ({ user }, request) => {
      // 您的逻辑
      console.log(`用户 ${user.email} 的密码已被重置。`);
    },
  },
});

避免等待邮件发送以防止时序攻击。在无服务器平台上,使用 waitUntil 或类似机制确保邮件发送成功。

此外,您可以提供 onPasswordReset 回调,在密码成功重置后执行逻辑。

服务器配置完成后,您可以调用 requestPasswordReset 函数向用户发送密码重置链接。如果用户存在,会触发您在认证配置中提供的 sendResetPassword 函数。

POST/request-password-reset
const { data, error } = await authClient.requestPasswordReset({    email: "john.doe@example.com", // required    redirectTo: "https://example.com/reset-password",});
Parameters
emailstringrequired

用于发送密码重置邮件的用户电子邮件地址

redirectTostring

用于重置密码的重定向 URL。如果令牌无效或过期,将带有查询参数 ?error=INVALID_TOKEN 重定向。有效则带有 ?token=VALID_TOKEN

用户点击邮件中的链接后,将重定向到密码重置页。您可以在应用中添加该页面,然后调用 resetPassword 函数完成密码重置。它接收一个包含以下属性的对象:

  • newPassword:用户的新密码。
import { authClient } from "@/lib/auth-client"

const { data, error } = await authClient.resetPassword({
  newPassword: "password1234",
  token,
});
POST/reset-password
const token = new URLSearchParams(window.location.search).get("token");if (!token) {  // 处理错误}const { data, error } = await authClient.resetPassword({    newPassword: "password1234", // required    token, // required});
Parameters
newPasswordstringrequired

要设置的新密码

tokenstringrequired

用于重置密码的令牌

更新密码

用户密码不存储在用户表中,而是存储于账户表内。要更改用户密码,您可以采用以下方案之一:

POST/change-password
const { data, error } = await authClient.changePassword({    newPassword: "newpassword1234", // required    currentPassword: "oldpassword1234", // required    revokeOtherSessions: true,});
Parameters
newPasswordstringrequired

要设置的新密码

currentPasswordstringrequired

当前用户密码

revokeOtherSessionsboolean

设为 true 时,撤销该用户所有其它活动会话

配置

密码

Better Auth 将密码存储在 account 表中,providerId 设置为 credential

密码哈希:Better Auth 使用 scrypt 来哈希密码。scrypt 算法设计为计算缓慢且占用内存,从而让攻击者难以破解密码。OWASP 建议在无 argon2id 时使用 scrypt。我们选择 scrypt 是因为其被 Node.js 原生支持。

您可以通过在 emailAndPassword 配置中设置 password 选项,传入自定义的密码哈希算法。

示例

下面演示如何自定义密码哈希使用 Argon2:

password.ts
import { hash, type Options, verify } from "@node-rs/argon2";

const opts: Options = {
  memoryCost: 65536, // 64 MiB
  timeCost: 3, // 3 次迭代
  parallelism: 4, // 4 个线程
  outputLen: 32, // 32 字节
  algorithm: 2, // Argon2id
};

export async function hashPassword(password: string) {
  const result = await hash(password, opts);
  return result;
}

export async function verifyPassword(data: { password: string; hash: string }) {
  const { password, hash } = data;
  const result = await verify(hash, password, opts);
  return result;
}
auth.ts
import { betterAuth } from "better-auth";
import { hashPassword, verifyPassword } from "./password";

export const auth = betterAuth({
  emailAndPassword: {
    //...其余选项
    enabled: true,
    password: {
      hash: hashPassword,
      verify: verifyPassword,
    },
  },
});

Prop

Type