数据库

适配器

Better Auth 需要一个数据库连接来存储数据。数据库将用于存储用户、会话等数据。插件也可以定义自己的数据库表来存储数据。

您可以通过在数据库选项中传递受支持的数据库实例,将数据库连接传递给 Better Auth。您可以在其他关系型数据库文档中了解更多关于支持的数据库适配器的信息。

CLI

Better Auth 附带了一个 CLI 工具来管理数据库迁移和生成模式。

运行迁移

CLI 会检查您的数据库,并提示您添加缺失的表或使用新列更新现有表。这仅适用于内置的 Kysely 适配器。对于其他适配器,您可以使用 generate 命令来创建模式并通过您的 ORM 处理迁移。

npx @better-auth/cli migrate

生成模式

Better Auth 还提供了一个 generate 命令来生成所需的模式。generate 命令会创建 Better Auth 所需的模式。如果您使用的是 Prisma 或 Drizzle 等数据库适配器,此命令将为您的 ORM 生成正确的模式。如果您使用的是内置的 Kysely 适配器,它将生成一个可以直接在数据库上运行的 SQL 文件。

npx @better-auth/cli generate

有关 CLI 的更多信息,请参阅 CLI 文档。

如果您更喜欢手动添加表,也可以这样做。Better Auth 所需的核心模式如下所述,您可以在插件文档中找到插件所需的附加模式。

辅助存储

Better Auth 中的辅助存储允许您使用键值存储来管理会话数据、速率限制计数器等。当您希望将这些密集型记录的存储卸载到高性能存储甚至 RAM 时,这会非常有用。

实现方式

要使用二级存储,需要实现 SecondaryStorage 接口:

interface SecondaryStorage {
  get: (key: string) => Promise<unknown>; 
  set: (key: string, value: string, ttl?: number) => Promise<void>;
  delete: (key: string) => Promise<void>;
}

然后,将你的实现提供给 betterAuth 函数:

betterAuth({
  // ... 其他选项
  secondaryStorage: {
    // 在此处提供你的实现
  },
});

示例:Redis 实现

以下是一个使用 Redis 的基础示例:

import { createClient } from "redis";
import { betterAuth } from "better-auth";

const redis = createClient();
await redis.connect();

export const auth = betterAuth({
	// ... 其他选项
	secondaryStorage: {
		get: async (key) => {
			return await redis.get(key);
		},
		set: async (key, value, ttl) => {
			if (ttl) await redis.set(key, value, { EX: ttl });
			// 或者对于 ioredis:
			// if (ttl) await redis.set(key, value, 'EX', ttl)
			else await redis.set(key, value);
		},
		delete: async (key) => {
			await redis.del(key);
		}
	}
});

此实现允许 Better Auth 使用 Redis 来存储会话数据和速率限制计数器。你还可以为键名添加前缀。

核心数据表结构

Better Auth 要求数据库中包含以下表。类型采用 typescript 格式。你可以在数据库中使用相应的类型。

用户表

表名:user

字段名称类型Key描述
idstring每个用户的唯一标识符
namestring-用户选择的显示名称
emailstring-用于通信和登录的用户邮箱地址
emailVerifiedboolean-用户邮箱是否已验证
imagestring用户头像的URL
createdAtDate-用户账户创建的时间戳
updatedAtDate-用户信息最后更新的时间戳

会话 (Session)

表名: session

字段名称类型Key描述
idstring每个会话的唯一标识符
userIdstring用户ID
tokenstring-唯一的会话令牌
expiresAtDate-会话过期时间
ipAddressstring设备的IP地址
userAgentstring设备的用户代理信息
createdAtDate-会话创建时间戳
updatedAtDate-会话更新时间戳

账户 (Account)

表名: account

字段名称类型Key描述
idstring每个账户的唯一标识符
userIdstring用户的 ID
accountIdstring-由 SSO 提供的账户 ID,或对于凭据账户等同于 userId
providerIdstring-提供商的 ID
accessTokenstring账户的访问令牌。由提供商返回
refreshTokenstring账户的刷新令牌。由提供商返回
accessTokenExpiresAtDate访问令牌的过期时间
refreshTokenExpiresAtDate刷新令牌的过期时间
scopestring账户的权限范围。由提供商返回
idTokenstring从提供商返回的 ID 令牌
passwordstring账户的密码。主要用于邮箱和密码认证
createdAtDate-账户创建的时间戳
updatedAtDate-账户更新的时间戳

验证

表名:verification

字段名称类型Key描述
idstring每个验证的唯一标识符
identifierstring-验证请求的标识符
valuestring-待验证的值
expiresAtDate-验证请求的过期时间
createdAtDate-验证请求创建的时间戳
updatedAtDate-验证请求更新的时间戳

自定义表

Better Auth 允许您为核心模式自定义表名和列名。您还可以通过向用户表和会话表添加额外字段来扩展核心模式。

自定义表名

您可以通过在认证配置中使用 modelNamefields 属性来自定义核心架构的表名和列名:

auth.ts
export const auth = betterAuth({
  user: {
    modelName: "users",
    fields: {
      name: "full_name",
      email: "email_address",
    },
  },
  session: {
    modelName: "user_sessions",
    fields: {
      userId: "user_id",
    },
  },
});

代码中的类型推断仍将使用原始字段名(例如 user.name,而不是 user.full_name)。

要为插件自定义表名和列名,您可以在插件配置中使用 schema 属性:

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

export const auth = betterAuth({
  plugins: [
    twoFactor({
      schema: {
        user: {
          fields: {
            twoFactorEnabled: "two_factor_enabled",
            secret: "two_factor_secret",
          },
        },
      },
    }),
  ],
});

扩展核心模式

Better Auth 提供了一种类型安全的方式来扩展 usersession 模式。您可以在认证配置中添加自定义字段,CLI 将自动更新数据库模式。这些额外字段将在 useSessionsignUp.email 以及其他处理用户或会话对象的函数中被正确推断。

要添加自定义字段,请在认证配置的 usersession 对象中使用 additionalFields 属性。additionalFields 对象使用字段名作为键,每个值都是一个 FieldAttributes 对象,包含:

  • type:字段的数据类型(例如:"string"、"number"、"boolean")。
  • required:布尔值,指示该字段是否为必填项。
  • defaultValue:字段的默认值(注意:这仅在 JavaScript 层应用;在数据库中,该字段将是可选的)。
  • input:这决定了在创建新记录时是否可以提供值(默认:true)。如果有额外的字段(如 role)不应在用户注册时由用户提供,可以将其设置为 false

以下是如何使用额外字段扩展用户模式的示例:

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

export const auth = betterAuth({
  user: {
    additionalFields: {
      role: {
        type: "string",
        required: false,
        defaultValue: "user",
        input: false, // 不允许用户设置角色
      },
      lang: {
        type: "string",
        required: false,
        defaultValue: "en",
      },
    },
  },
});

现在您可以在应用程序逻辑中访问这些额外字段。

// 注册时
const res = await auth.api.signUpEmail({
  email: "test@example.com",
  password: "password",
  name: "John Doe",
  lang: "fr",
});

// 用户对象
res.user.role; // > "admin"
res.user.lang; // > "fr"

有关如何在客户端推断额外字段的更多信息,请参阅 TypeScript 文档。

如果您使用社交/OAuth 提供商,可能需要提供 mapProfileToUser 来将配置文件数据映射到用户对象。这样,您可以从提供商的配置文件中填充额外字段。

示例:将用户资料映射到用户的 firstNamelastName

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

export const auth = betterAuth({
  socialProviders: {
    github: {
      clientId: "YOUR_GITHUB_CLIENT_ID",
      clientSecret: "YOUR_GITHUB_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.name.split(" ")[0],
          lastName: profile.name.split(" ")[1],
        };
      },
    },
    google: {
      clientId: "YOUR_GOOGLE_CLIENT_ID",
      clientSecret: "YOUR_GOOGLE_CLIENT_SECRET",
      mapProfileToUser: (profile) => {
        return {
          firstName: profile.given_name,
          lastName: profile.family_name,
        };
      },
    },
  },
});

ID 生成

Better Auth 默认会为用户、会话和其他实体生成唯一的 ID。如果你想自定义 ID 的生成方式,可以在认证配置中的 advanced.database.generateId 选项中进行配置。

你也可以通过将 advanced.database.generateId 选项设置为 false 来禁用 ID 生成。这将假设你的数据库会自动生成 ID。

示例:自动数据库 ID

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

export const auth = betterAuth({
  database: {
    db: db,
  },
  advanced: {
    database: {
      generateId: false,
    },
  },
});

数据库钩子

数据库钩子允许你在 Better Auth 的核心数据库操作生命周期中定义并执行自定义逻辑。你可以为以下模型创建钩子:用户(user)会话(session)账户(account)

你可以定义两种类型的钩子:

1. Before Hook(前置钩子)

  • 用途:此钩子在相应实体(用户、会话或账户)被创建或更新之前调用。
  • 行为:如果钩子返回 false,操作将被中止。如果返回数据对象,它将替换原始的有效载荷。

2. After Hook(后置钩子)

  • 用途:此钩子在相应实体被创建或更新之后调用。
  • 行为:你可以在实体成功创建或更新后执行额外的操作或修改。

示例用法

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

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          // 在用户对象创建前进行修改
          return {
            data: {
              ...user,
              firstName: user.name.split(" ")[0],
              lastName: user.name.split(" ")[1],
            },
          };
        },
        after: async (user) => {
          // 执行额外操作,例如创建 Stripe 客户
        },
      },
    },
  },
});

抛出错误

如果你想阻止数据库钩子继续执行,可以使用从 better-auth/api 导入的 APIError 类抛出错误。

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

export const auth = betterAuth({
  databaseHooks: {
    user: {
      create: {
        before: async (user, ctx) => {
          if (user.isAgreedToTerms === false) {
            // 你的特殊条件
            // 发送 API 错误
            throw new APIError("BAD_REQUEST", {
              message: "用户注册前必须同意服务条款。",
            });
          }
          return {
            data: user,
          };
        },
      },
    },
  },
});

使用上下文对象

上下文对象(ctx)作为钩子的第二个参数传递,包含有用的信息。对于 update 钩子,这包括当前的 session,您可以使用它来访问已登录用户的详细信息。

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

export const auth = betterAuth({
  databaseHooks: {
    user: {
      update: {
        before: async (data, ctx) => {
          // 您可以从上下文对象访问会话信息。
          if (ctx.context.session) {
            console.log("用户更新由以下用户发起:", ctx.context.session.userId);
          }
          return { data };
        },
      },
    },
  },
});

与标准钩子类似,数据库钩子也提供了一个 ctx 对象,该对象提供了多种有用的属性。了解更多信息,请参阅钩子文档

插件模式

插件可以在数据库中定义自己的表来存储额外数据。它们还可以向核心表添加列以存储额外数据。例如,双因素认证插件向 user 表添加了以下列:

  • twoFactorEnabled:用户是否启用了双因素认证。
  • twoFactorSecret:用于生成 TOTP 码的密钥。
  • twoFactorBackupCodes:用于账户恢复的加密备份码。

要向数据库添加新表和列,您有两种选择:

CLI:使用迁移或生成命令。这些命令将扫描您的数据库,并指导您添加任何缺失的表或列。 手动方法:按照插件文档中的说明手动添加表和列。

这两种方法都能确保您的数据库模式与插件的要求保持同步。