插件

了解如何使用和创建 Better Auth 插件,包括定义端点、模式、钩子、中间件、速率限制、受信任来源,以及使用自定义操作和原子构建客户端插件。

插件是 Better Auth 的关键部分,它们允许您扩展基础功能。您可以使用插件添加新的身份验证方法、功能,或者自定义行为。

Better Auth 自带许多内置插件,随时可用。请查看插件章节了解详情。您也可以创建自己的插件。

使用插件

插件可以是服务器端插件、客户端插件,或者两者兼有。

要在服务器端添加插件,将其包含在认证配置的 plugins 数组中。插件将使用提供的选项进行初始化。

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

export const auth = betterAuth({
    plugins: [
        // 在此添加您的插件
    ]
});

客户端插件在创建客户端时添加。大多数插件需要服务器端和客户端插件共同配合才能正常工作。 前端的 Better Auth 认证客户端使用 better-auth/client 提供的 createAuthClient 函数。

auth-client.ts
import { createAuthClient } from "better-auth/client";

const authClient = createAuthClient({
    plugins: [
        // 在此添加您的客户端插件
    ]
});

我们建议将认证客户端和您的常规认证实例保存在单独的文件中。

server.ts
auth-client.ts

创建插件

首先,您需要一个服务器插件。 服务器插件是所有插件的核心,客户端插件提供与前端 API 的接口,便于与服务器插件协作。

如果您的服务器插件有需要从客户端调用的端点,您还需要创建客户端插件。

插件能做什么?

  • 创建自定义的 endpoint 执行任意操作。
  • 使用自定义的 schemas 扩展数据库表。
  • 使用 middleware 针对一组路由,利用路由匹配器,仅在通过请求调用这些路由时运行。
  • 使用 hooks 针对特定路由或请求。如果您希望即使直接调用端点也能执行钩子。
  • 如果想做影响所有请求或响应的事情,可以使用 onRequestonResponse
  • 创建自定义的 rate-limit 规则。

创建服务器插件

创建服务器插件时,需要传入满足 BetterAuthPlugin 接口的对象。

唯一必填属性是 id,它是插件唯一标识符。 服务器端与客户端插件可以使用相同的 id

plugin.ts
import type { BetterAuthPlugin } from "better-auth";

export const myPlugin = () => {
    return {
        id: "my-plugin",
    } satisfies BetterAuthPlugin
}

您不必将插件定义为函数,但建议这样做。这样,您可以向插件传递选项,并且与内置插件保持一致。

端点

要向服务器添加端点,可以传入 endpoints,它是一个对象,键是任意字符串,值为 AuthEndpoint

创建 Auth 端点需要从 better-auth 导入 createAuthEndpoint

Better Auth 封装了另一个名为 Better Call 的库来创建端点。Better Call 是由 Better Auth 团队开发的简单 TS Web 框架。

plugin.ts
import { createAuthEndpoint } from "better-auth/api";
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        endpoints: {
            getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
                method: "GET",
            }, async(ctx) => {
                return ctx.json({
                    message: "Hello World"
                })
            })
        }
    } satisfies BetterAuthPlugin
}

创建 Auth 端点实际上封装了 Better Call 的 createEndpoint。在 ctx 对象内部,还会提供一个名为 context 的对象,供您访问 Better Auth 特定的上下文,包括 optionsdbbaseURL 等。

Context 对象

  • appName: 应用程序的名称。默认为 "Better Auth"。
  • options: 传递给 Better Auth 实例的选项。
  • tables: 核心表定义。它是一个对象,以表名为键,模式定义为值。
  • baseURL: 认证服务器的 baseURL。这包括路径。例如,如果服务器运行在 http://localhost:3000,那么 baseURL 将是 http://localhost:3000/api/auth,除非用户更改。
  • session: 会话配置。包括 updateAgeexpiresIn 值。
  • secret: 用于各种目的的秘密密钥。这由用户定义。
  • authCookie: 核心认证 cookie 的默认配置。
  • logger: Better Auth 使用的日志记录器实例。
  • db: Better Auth 用于与数据库交互的 Kysely 实例。
  • adapter: 这与 db 相同,但它提供了类似 ORM 的函数来与数据库交互。(我们建议使用这个而不是 db,除非您需要原始 SQL 查询或出于性能原因)。
  • internalAdapter: 这些是 Better Auth 内部使用的数据库调用。例如,您可以使用这些调用来创建会话,而不是直接使用 adapterinternalAdapter.createSession(userId)
  • createAuthCookie: 这是一个帮助函数,让您获取用于 setget cookie 的 cookie nameoptions。它实现了基于连接是否安全(HTTPS)或应用程序是否运行在生产模式下的 __Secure- 前缀等功能。
  • trustedOrigins: 这是您通过 options.trustedOrigins 指定的受信任来源列表。
  • isTrustedOrigin: 这是一个帮助函数,允许您快速检查给定的 URL 或路径是否基于受信任来源配置受信任。

要查看其他属性,请参考 Better Call 文档和 源代码

端点规则

  • 确保端点路径使用连字符命名法(kebab-case)
  • 确保仅对端点使用 POSTGET 方法。
  • 任何修改数据的函数都应使用 POST 方法。
  • 任何获取数据的函数都应使用 GET 方法。
  • 确保使用 createAuthEndpoint 函数创建 API 端点。
  • 确保路径唯一以避免与其他插件冲突。如果使用通用路径,请在路径前添加插件名称(/my-plugin/hello-world 而不是 /hello-world)。

您可以通过传入 schema 对象定义插件的数据库模式。schema 对象以表名为键,模式定义为值。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        schema: {
            myTable: {
                fields: {
                    name: {
                        type: "string"
                    }
                },
                modelName: "myTable" // 如需与键名不同,可指定,否则可省略
            }
        }
    } satisfies BetterAuthPlugin
}

字段

默认 Better Auth 会为每张表创建一个 id 字段。您可以通过向 fields 对象添加字段,自定义额外列。

字段名作为键,字段定义为值。定义属性包括:

  • type:字段类型,可选 stringnumberbooleandate
  • required:是否为必填字段,(默认 true
  • unique:字段是否唯一,(默认 false
  • references:字段是否为关联另一张表的外键(可选),对象格式包含:
    • model:所关联表名
    • field:所关联字段名
    • onDelete:关联记录删除时操作(默认 cascade

其他 Schema 属性

unique: if the field should be unique. (default: false)

references: if the field is a reference to another table. (optional) It takes an object with the following properties:

  • model: The table name to reference.
  • field: The field name to reference.
  • onDelete: The action to take when the referenced record is deleted. (default: cascade)

其他 Schema 属性

disableMigration: if the table should not be migrated. (default: false)

plugin.ts
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = (opts: PluginOptions) => {
    return {
        id: "my-plugin",
        schema: {
            rateLimit: {
                fields: {
                    key: {
                        type: "string",
                    },
                },
                disableMigration: opts.storage.provider !== "database", 
            },
        },
    } satisfies BetterAuthPlugin
}

如果您在 usersession 表添加额外字段,getSessionsignUpEmail 调用时类型会自动推断。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        schema: {
            user: {
                fields: {
                    age: {
                        type: "number",
                    },
                },
            },
        },
    } satisfies BetterAuthPlugin
}

这样会为 user 表添加 age 字段,所有返回 user 的接口也会包含此字段,且 TypeScript 会正确推断。

不要在 usersession 表中存储敏感信息。如需存储敏感信息,请创建新表。

钩子(Hooks)

钩子用于在操作执行前后运行代码,无论是从客户端还是直接在服务器上执行。您可以通过传入 hooks 对象添加钩子,该对象须包含 beforeafter 属性。

plugin.ts
import { createAuthMiddleware } from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        hooks: {
            before: [{
                    matcher: (context) => {
                        return context.headers.get("x-my-header") === "my-value"
                    },
                    handler: createAuthMiddleware(async (ctx) => {
                        // 请求前做一些操作
                        return  {
                            context: ctx // 如需修改上下文
                        }
                    })
                }],
            after: [{
                matcher: (context) => {
                    return context.path === "/sign-up/email"
                },
                handler: createAuthMiddleware(async (ctx) => {
                    return ctx.json({
                        message: "Hello World"
                    }) // 如需修改响应
                })
            }]
        }
    } satisfies BetterAuthPlugin
}

中间件

您可以通过传入 middlewares 数组给服务器添加中间件。数组中每项为中间件对象,包含 pathmiddleware 属性。与钩子不同,中间件仅在客户端发起的 API 请求时执行,若直接调用端点,中间件不会运行。

path 可以为字符串或路径匹配器,使用与 better-call 相同的路径匹配系统。

中间件内若抛出 APIError 或返回 Response 对象,请求将停止,响应发送给客户端。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";
import { createAuthMiddleware } from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        middlewares: [
            {
                path: "/my-plugin/hello-world",
                middleware: createAuthMiddleware(async(ctx) => {
                    // 执行操作
                })
            }
        ]
    } satisfies BetterAuthPlugin
}

On Request & On Response

请求前

onRequest 函数在请求发起前执行,接受两个参数:requestcontext

用法示例:

Here’s how it works:

  • Continue as Normal: 如果您不返回任何内容,请求将按正常流程继续。
  • Interrupt the Request: 要停止请求并发送响应,请返回一个包含 response 属性的对象,该属性包含一个 Response 对象。
  • Modify the Request: 您还可以返回一个修改后的 request 对象,以在请求发送前更改请求。
plugin.ts
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return  {
        id: "my-plugin",
        onRequest: async (request, context) => {
            // 执行操作
        },
    } satisfies BetterAuthPlugin
}

响应后

onResponse 函数在响应返回后立即执行,接受两个参数:responsecontext

用法示例:

  • Modify the Response: 您可以返回一个修改后的响应对象以更改响应,然后再发送给客户端。
  • Continue Normally: 如果您不返回任何内容,响应将按原样发送。
plugin.ts
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        onResponse: async (response, context) => {
            // 执行操作
        },
    } satisfies BetterAuthPlugin
}

限速规则

您可以通过传入 rateLimit 数组定义自定义限速规则。数组项为限速对象。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";

const myPlugin = () => {
    return {
        id: "my-plugin",
        rateLimit: [
            {
                pathMatcher: (path) => {
                    return path === "/my-plugin/hello-world"
                },
                limit: 10,
                window: 60,
            }
        ]
    } satisfies BetterAuthPlugin
}

受信任来源

如果您正在构建自定义插件或端点,可以使用认证上下文中的 isTrustedOrigin() 方法根据受信任来源配置验证 URL。这样确保自定义端点遵循与 Better Auth 内置端点相同的安全设置。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";
import { createAuthEndpoint, APIError } from "better-auth/api";
import * as z from "zod"

const myPlugin = () => {
    return {
        id: "my-plugin",
        trustedOrigins: [
            "http://trusted.com"
        ],
        endpoints: {
            getTrustedHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
                method: "GET",
                query: z.object({
                    url: z.string()
                }),
            }, async (ctx) => {
                // allowRelativePaths 选项用于允许或禁止相对路径
                if (!ctx.context.isTrustedOrigin(ctx.query.url, { allowRelativePaths: false })) {
                    throw new APIError("FORBIDDEN", {
                        message: "origin is not trusted."
                    });
                }

                return ctx.json({
                    message: "Hello World"
                })
            })
        }
    } satisfies BetterAuthPlugin
}

更多信息请参见受信任来源和安全文档。

服务器插件辅助函数

下面是一些创建服务器插件的辅助函数。

getSessionFromCtx

通过传入认证中间件的 context,可获取客户端会话数据。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";
import { createAuthMiddleware, getSessionFromCtx } from "better-auth/api";

const myPlugin = {
    id: "my-plugin",
    hooks: {
        before: [{
                matcher: (context) => {
                    return context.headers.get("x-my-header") === "my-value"
                },
                handler: createAuthMiddleware(async (ctx) => {
                    const session = await getSessionFromCtx(ctx);
                    // 操作客户端的会话数据

                    return  {
                        context: ctx
                    }
                })
            }],
    }
} satisfies BetterAuthPlugin

sessionMiddleware

此中间件用于检查客户端是否具有有效会话,若有效,则会将会话数据添加到上下文对象中。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";
import { createAuthEndpoint, sessionMiddleware } from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        endpoints: {
            getHelloWorld: createAuthEndpoint("/my-plugin/hello-world", {
                method: "GET",
                use: [sessionMiddleware], 
            }, async (ctx) => {
                const session = ctx.context.session;
                return ctx.json({
                    message: "Hello World"
                })
            })
        }
    } satisfies BetterAuthPlugin
}

requireResourceOwnership

一个中间件,用于通过 ID 加载资源并验证其属于认证用户。在 sessionMiddleware 之后用于操作用户拥有的模型的端点。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";
import * as z from "zod";
import {
    createAuthEndpoint,
    requireResourceOwnership,
    sessionMiddleware,
} from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        endpoints: {
            deleteApiKey: createAuthEndpoint("/my-plugin/delete-api-key", {
                method: "POST",
                body: z.object({
                    id: z.string(),
                }),
                use: [
                    sessionMiddleware,
                    requireResourceOwnership({
                        model: "apiKey",
                        idParam: "id",
                        idSource: "body",
                    }),
                ],
            }, async (ctx) => {
                return ctx.json({ success: true });
            })
        }
    } satisfies BetterAuthPlugin
}

如果您的模型在不同字段名下存储所有者,请使用 ownerField,如果要返回插件特定的错误代码,请使用 notFoundErrorforbiddenError

requireOrgRole

一个中间件,用于验证认证用户是否是特定组织的成员,并且(可选)具有一组允许的角色之一。在组织范围的端点中,在 sessionMiddleware 之后使用。

plugin.ts
import type { BetterAuthPlugin } from "better-auth";
import * as z from "zod";
import {
    createAuthEndpoint,
    requireOrgRole,
    sessionMiddleware,
} from "better-auth/api";

const myPlugin = () => {
    return {
        id: "my-plugin",
        endpoints: {
            syncDirectory: createAuthEndpoint("/my-plugin/sync-directory", {
                method: "POST",
                body: z.object({
                    organizationId: z.string(),
                }),
                use: [
                    sessionMiddleware,
                    requireOrgRole({
                        orgIdParam: "organizationId",
                        orgIdSource: "body",
                        allowedRoles: ["admin", "owner"],
                    }),
                ],
            }, async (ctx) => {
                return ctx.json({ success: true });
            })
        }
    } satisfies BetterAuthPlugin
}

如果省略 allowedRoles,则接受任何组织成员。具有多个角色的成员在任一角色匹配时即被授权。

创建客户端插件

如果您的端点需要从客户端调用,您还需要创建一个客户端插件。Better Auth 客户端可以从服务器插件推断端点。您也可以添加额外的客户端逻辑。

client-plugin.ts
import type { BetterAuthClientPlugin } from "better-auth/client";

export const myPluginClient = () => {
    return {
        id: "my-plugin",
    } satisfies BetterAuthClientPlugin
}

端点接口

客户端插件添加 $InferServerPlugin 字段,用于推断服务器端插件的端点。

客户端将路径作为对象,并将 kebab-case 转换为 camelCase。例如,/my-plugin/hello-world 转为 myPlugin.helloWorld

client-plugin.ts
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";

const myPluginClient = () => {
    return  {
        id: "my-plugin",
        $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
    } satisfies BetterAuthClientPlugin
}

获取 actions

如需为客户端添加额外方法,可使用 getActions 函数,它接收客户端的 fetch 函数。

Better Auth 使用 Better Fetch 进行请求,它是作者开发的简单 fetch 包装器。

client-plugin.ts
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";
import type { BetterFetchOption } from "@better-fetch/fetch";

const myPluginClient = {
    id: "my-plugin",
    $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
    getActions: ($fetch) => {
        return {
            myCustomAction: async (data: {
                foo: string,
            }, fetchOptions?: BetterFetchOption) => {
                const res = $fetch("/custom/action", {
                    method: "POST",
                    body: {
                        foo: data.foo
                    },
                    ...fetchOptions
                })
                return res
            }
        }
    }
} satisfies BetterAuthClientPlugin

一般指导原则是确保每个函数只接受一个参数,并使用第二个可选的 fetchOptions 参数,以便用户可以向 fetch 调用传递额外选项。该函数应返回一个包含 dataerror 键的对象。

如果您的用例涉及超出 API 调用的操作,请随意偏离此规则。

获取 Atom

此功能仅在您想提供如 useSession 这类钩子时有用。

getAtoms 函数接收 Better Fetch 的 fetch,返回包含各个 Atom 的对象。Atom 应使用 nanostores 创建。Atom 会被各框架基于 nanostores 的 useStore 钩子自动解析。

client-plugin.ts
import { atom } from "nanostores";
import type { BetterAuthClientPlugin } from "better-auth/client";

const myPluginClient = {
    id: "my-plugin",
    $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
    getAtoms: ($fetch) => {
        const myAtom = atom<null>()
        return {
            myAtom
        }
    }
} satisfies BetterAuthClientPlugin

请参阅内置插件了解如何正确使用 atoms。

路径方法

默认情况下,推断的路径如果不需要请求体则使用 GET,否则使用 POST。您可以通过传入 pathMethods 对象覆盖,键为路径,值为方法("POST" | "GET")。

client-plugin.ts
import type { BetterAuthClientPlugin } from "better-auth/client";
import type { myPlugin } from "./plugin";

const myPluginClient = {
    id: "my-plugin",
    $InferServerPlugin: {} as ReturnType<typeof myPlugin>,
    pathMethods: {
        "/my-plugin/hello-world": "POST"
    }
} satisfies BetterAuthClientPlugin

Fetch 插件

如需使用 Better Fetch 插件,可以传入 fetchPlugins 数组。更多信息请参阅 Better Fetch 文档

Atom 监听器

此功能只有在您想提供如 useSession 的钩子,并且希望监听 atoms 并在改变时重新计算时才有用。

内置插件中有示例展示了此用法。