Better Auth 集成实战指南:TypeScript 认证框架的配置、会话、插件与真实工程接线解析
【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu
Better Auth(better-auth)是一个 TypeScript 优先、框架无关的全栈认证框架,通过插件体系支持邮箱/密码、OAuth 社交登录、魔术链接(magic link)、Passkey、双因素认证等多种能力。本仓库在.cursor/skills/better-auth-best-practices/SKILL.md中收录了面向开发与 AI 编码代理的集成最佳实践速查技能。本文将这份技能文档作为核心骨架,完整展开其配置项、CLI 命令、数据库适配、会话策略、钩子与插件体系,并结合仓库中仪表盘(dashboard)与 API 服务的真实接线代码,说明 Better Auth 如何被落地到一个生产级的多租户应用中,帮助你从零到一完成集成、并理解背后的关键决策点。
一、整体定位与核心概念
Better Auth 的核心设计理念是:认证能力由一组可组合的插件(plugin)提供,而不是堆在核心模块里。邮箱密码登录、OAuth、组织多租户、双因素、SSO 等能力各自独立成插件,按需启用即可获得对应的数据库表、API 端点与客户端方法。
在深入研究任何细节前,需要始终记住一个原则:技能文档要求"始终以官方文档与最新 API 为准"(always consult better-auth.com/docs for code examples and latest API)。本文的所有条目均基于当前仓库收录的技能版本整理,若未来升级,应以官方文档、Options Reference 与LLMs.txt中的最新内容为准。官方还建议在启用新插件或修改配置后,重新审视本技能中"数据库、会话、钩子"三节,因为插件会改变数据模型与可用端点。
二、快速参考:环境变量、文件位置与 CLI 命令
1. 环境变量
| 环境变量 | 作用 | 生成方式 |
|---|---|---|
BETTER_AUTH_SECRET | 会话/加密密钥,最少 32 个字符 | openssl rand -base64 32 |
BETTER_AUTH_URL | 服务的基础 URL(如https://example.com) | — |
技能文档给出的铁律是:只有在环境变量未设置时,才在配置对象里手动填写baseURL/secret。也就是说环境变量优先于代码内配置,这既便于不同环境(dev/staging/prod)共享同一份代码,也避免了把密钥硬编码进仓库。
2. 配置文件位置
Better Auth CLI 会自动在以下目录寻找名为auth.ts的文件:
././lib./utils./src(即./src下)
如果配置文件不在上述约定位置,使用--config参数显式指定自定义路径。
3. CLI 命令
| 命令 | 用途 |
|---|---|
npx @better-auth/cli@latest migrate | 内建数据库适配器场景下直接执行 schema 迁移 |
npx @better-auth/cli@latest generate | 为 Prisma / Drizzle 等 ORM 生成 schema 定义 |
npx @better-auth/cli mcp --cursor | 将 Better Auth 文档以 MCP(Model Context Protocol)形式接入 AI 编码工具 |
重要提醒:在新增或修改插件之后,必须重新运行上述 CLI 命令。因为插件会带来新的数据表、字段和配置类型,只有重新生成/迁移,schema 与类型定义才能与实际运行的代码保持一致(这也是第六节"常见陷阱"中第 2 条的来源)。
三、核心配置选项详解
技能文档给出的核心配置选项速查表如下,下表在保留原文全部字段的基础上补充了实践注记:
| 配置选项 | 说明与注意点 |
|---|---|
appName | 可选的展示名称,用于界面与邮件等场景 |
baseURL | 仅在未设置BETTER_AUTH_URL时才需要配置,否则应从环境变量读取 |
basePath | 默认/api/auth;若想挂在站点根路径可设为/ |
secret | 仅在未设置BETTER_AUTH_SECRET时才需要配置 |
database | 大多数功能(会话、账户等)的必需品,详见第四节"数据库" |
secondaryStorage | Redis / KV 类存储,用于会话与限流计数,启用后会话默认写入这里而非数据库 |
emailAndPassword | 形如{ enabled: true },开启后激活邮箱密码登录端点 |
socialProviders | 社交登录提供方,如{ google: { clientId, clientSecret }, ... } |
plugins | 插件数组,按需组合认证能力 |
trustedOrigins | CSRF 白名单,用于允许跨源认证请求 |
需要特别说明的是basePath:它决定了认证端点的 URL 前缀。生产中大型单体服务通常不会把认证挂在应用根路径上,而是挂在某个统一 API 前缀下(见第九节的 Novu 实践:通过配置baseURL指向.../v1/better-auth完成路径收编)。而trustedOrigins与"安全"一节的disableCSRFCheck/disableOriginCheck直接相关:跨域部署、前后端分离架构下,认证请求的Origin校验必须与白名单精确匹配。
四、数据库:直连、ORM 适配器与"模型名陷阱"
1. 两种连接方式
Better Auth 的database配置支持两种接入形态:
- 直接连接(driver 实例):直接传入
pg.Pool、mysql2pool、better-sqlite3或bun:sqlite实例。适合 Node 服务直连数据库、不想引入 ORM 的场景。 - ORM 适配器(推荐用于已有 ORM 栈):从专用子路径导入,例如:
better-auth/adapters/drizzlebetter-auth/adapters/prismabetter-auth/adapters/mongodb
2. 关键陷阱:模型名 ≠ 表名
技能文档用加粗体强调了一个极易踩坑的点:
Better Auth 使用的是"适配器模型名(adapter model name)",而不是底层表名。
举例:如果 Prisma 中的 model 名为User、映射到的数据库表名为users,那么在配置中引用该模型时应写modelName: "user"(对应 Prisma 的模型引用名,采用单数),绝不能写成"users"(那是表名)。所有涉及用户、会话、账户、验证令牌等实体的modelName配置,都应遵循"模型名"语义。这个约定贯穿user.modelName、account.modelName、session相关配置以及databaseHooks中的实体引用。
五、会话管理:存储优先级、无状态模式与 Cookie 缓存策略
会话是认证框架的核心,Better Auth 的会话存储遵循一条优先级链:
- 如果配置了
secondaryStorage:会话默认写入 secondary storage(如 Redis),而不是数据库; - 如果希望同时持久化到数据库:设置
session.storeSessionInDatabase: true; - 无数据库 + 配置
cookieCache:进入完全无状态模式——会话只存在于 Cookie 中,服务端不保存任何会话记录。
Cookie 缓存(cookieCache)三种策略
| 策略 | 说明 | 特点 |
|---|---|---|
compact(默认) | Base64url + HMAC | 体积最小,适合 Cookie 尺寸受限场景 |
jwt | 标准 JWT | 可读(载荷可见)但带签名 |
jwe | 加密 JWT | 载荷不可读,安全性最高 |
关键会话选项
session.expiresIn:会话有效期,默认 7 天;session.updateAge:会话"刷新间隔"——会话在该时间后刷新以延长寿命;session.cookieCache.maxAge:Cookie 缓存本身的存活时长;session.cookieCache.version:缓存版本号,一旦变更,会使所有已签发的会话 Cookie 失效(可用于全站强制登出的"核按钮")。
结合无状态模式需要注意:无数据库时,会话仅存于 Cookie,登出依赖 Cookie 过期/失效(见第六节陷阱 5)。
六、用户与账户配置
用户(User)
user.modelName:指定用户实体对应的模型名(见第四节"模型名陷阱");user.fields:字段/列映射,用于把框架要求的字段映射到 ORM 中实际命名的字段;user.additionalFields:为 user 表追加自定义业务字段;user.changeEmail.enabled:是否允许用户更换邮箱,默认关闭;user.deleteUser.enabled:是否允许用户自助删除账号,默认关闭。
账户(Account)
account.modelName:账户实体模型名;account.accountLinking.enabled:是否启用账户关联(同一用户绑定多个 OAuth/邮箱身份);account.storeAccountCookie:为无状态 OAuth场景把账户信息写入 Cookie。
注册字段约束
启用邮箱密码注册时,email与name两个字段为必填项。如果采用自建注册流程,需要保证提交载荷中携带这两个字段,否则注册会被拒绝。
七、邮件流:验证、注册/登录触发与密码重置
Better Auth 默认不发送任何邮件,所有邮件发送动作都必须由开发者显式实现为 handler。技能文档列出的三类邮件入口:
| 配置项 | 作用 |
|---|---|
emailVerification.sendVerificationEmail | 发送验证邮件的处理器。必须定义它,邮箱验证功能才会生效 |
emailVerification.sendOnSignUp | 注册成功后自动触发发送验证邮件 |
emailVerification.sendOnSignIn | 登录时(若未验证)自动触发发送验证邮件 |
emailAndPassword.sendResetPassword | 发送密码重置邮件的处理器 |
也就是说:sendVerificationEmail与sendResetPassword是"发信函数"的挂载点(函数内部由你接入自己的邮件服务);而sendOnSignUp/sendOnSignIn是"何时自动发送"的开关。在本仓库的登录页实现中可以看到对应客户端侧的方法调用链——登录失败且用户未验证时,前端会调用authClient.sendVerificationEmail({ email, callbackURL })让用户重发验证邮件(见 apps/dashboard/src/utils/better-auth/components/sign-in.tsx#L26-L43)。
八、安全与高级选项
advanced区块内的选项
| 选项 | 说明 |
|---|---|
useSecureCookies | 强制 Cookie 仅走 HTTPS(生产环境必须开启) |
disableCSRFCheck | 关闭 CSRF 校验——技能文档标注 ⚠️安全风险 |
disableOriginCheck | 关闭来源(Origin)校验——技能文档标注 ⚠️安全风险 |
crossSubDomainCookies.enabled | 允许多个子域共享 Cookie(如app.example.com与api.example.com) |
ipAddress.ipAddressHeaders | 自定义 IP 请求头,用于反代/网关场景下的真实 IP 识别(否则取到的是代理地址) |
database.generateId | 自定义 ID 生成策略,可传函数或"serial"/"uuid"/false |
限流(Rate limiting)
rateLimit.enabled:总开关;rateLimit.window:限流时间窗口;rateLimit.max:窗口内最大请求数;rateLimit.storage:限流计数的存放位置,取值"memory" | "database" | "secondary-storage"。
合理选择rateLimit.storage值得注意:"memory"仅适合单实例开发环境;多实例部署时计数不共享,应改用"database"或"secondary-storage"(如 Redis)。这也与第二节中secondaryStorage的定位相呼应——Redis/KV 同时承担会话与限流两类高频读写。
九、Hooks:在认证生命周期中插入逻辑
端点级 Hooks(hooks.before/hooks.after)
端点级钩子用于在具体认证端点(如 sign-in、sign-up、验证端点)执行前/后介入。其形态是:
{ hooks: { before: [{ matcher: "/sign-in/email", handler: createAuthMiddleware(async (ctx) => { ... }) }], after: [{ matcher: "/sign-in/email", handler: ... }], } }每个钩子项由{ matcher, handler }组成。handler 通过createAuthMiddleware创建,在中间件上下文中可访问:
ctx.path:当前请求路径;ctx.context.returned:仅在after钩子中可用,是端点返回值;ctx.context.session:当前会话对象。
数据库 Hooks(databaseHooks)
数据库钩子挂在实体写操作前后,支持user、session、account三类实体,形如:
databaseHooks.user.create.before/databaseHooks.user.create.afterdatabaseHooks.session.create.before/afterdatabaseHooks.account.create.before/after
典型用途包括:写入前填充默认值、写入后触发外部副作用(审计、通知、同步到业务库等)。
Hook 上下文(ctx.context)可用的能力清单
技能文档完整罗列了上下文对象中可直接调用的能力:
| 成员 | 作用 |
|---|---|
session | 当前会话数据 |
secret | 配置的认证密钥 |
authCookies | 认证相关 Cookie 的读写封装 |
password.hash()/password.verify() | 密码哈希与校验(自定义登录/迁移场景极有用) |
adapter | 底层数据适配器(直接读写任意模型) |
internalAdapter | 框架内部适配器(可操作 user/session/account/verification 等内部模型) |
generateId() | ID 生成器(可访问自定义 ID 策略) |
tables | 各模型的表/模型定义引用 |
baseURL | 当前配置的基础 URL |
十、插件体系:按需组合认证能力
导入规范:必须使用专用子路径
为保证 tree-shaking(按需打包)生效,插件必须从专用路径导入:
import { twoFactor } from "better-auth/plugins/two-factor";禁止写成from "better-auth/plugins"(整包导入会破坏 tree-shaking,把全部插件打进产物)。这是技能文档明确强调的规范。
常用插件一览
twoFactor(双因素)、organization(组织/多租户)、passkey、magicLink(魔术链接)、emailOtp(邮箱验证码)、username、phoneNumber、admin(管理后台)、apiKey、bearer(Bearer Token)、jwt、multiSession(多会话)、sso(企业单点登录)、oauthProvider、oidcProvider(把 Better Auth 自身作为 OAuth/OIDC 提供方)、openAPI(生成 OpenAPI 文档)、genericOAuth(通用 OAuth)。
客户端插件
服务端插件与客户端插件是配套的:客户端插件要放到createAuthClient({ plugins: [...] })中,才能让authClient暴露对应的客户端方法(如组织插件的organization.list()、SSO 插件的跳转与回调处理)。
仓库实战:organization + sso 插件
本仓库的仪表盘客户端就是一个插件化组合的实例。在 apps/dashboard/src/utils/better-auth/client.ts 中:
import { ssoClient } from '@better-auth/sso/client'; import { organizationClient } from 'better-auth/client/plugins'; import { createAuthClient } from 'better-auth/react'; const baseURL = BETTER_AUTH_BASE_URL || API_HOSTNAME || 'http://localhost:3000'; export const BETTER_AUTH_API_URL = `${baseURL}/v1/better-auth`; export const authClient = createAuthClient({ baseURL: BETTER_AUTH_API_URL, plugins: [organizationClient(), ssoClient()], // ... });可以看到:组织插件(organizationClient)与 SSO 插件(ssoClient)同时被挂到createAuthClient上,随后页面层即可直接调用组织相关方法。在 apps/dashboard/src/utils/better-auth/index.tsx 中,认证提供方通过authClient.useSession()维护会话,并在存在sessionData?.session?.activeOrganizationId时调用authClient.organization.getFullOrganization(...)拉取当前组织及其成员角色、authClient.organization.list()罗列用户所属组织、authClient.organization.setActive(...)切换当前组织,signOut()则负责退出并清理本地令牌。这正是"插件 = 数据表 + 服务端端点 + 客户端方法"三位一体模型的直观印证。
十一、客户端接入:框架绑定、会话 API 与令牌管理
按框架选择导入路径
Better Auth 客户端按使用场景提供多种入口:
better-auth/client:原生 vanilla JS/TS;better-auth/react:React(Hooks);better-auth/vue:Vue;better-auth/svelte:Svelte;better-auth/solid:Solid。
核心方法
技能文档列出的关键 API:
signUp.email():邮箱注册;signIn.email():邮箱登录;signIn.social():社交账号登录;signOut():登出;useSession():React Hook 形态的会话订阅(服务端组件可用getSession());getSession():主动获取会话;revokeSession()/revokeSessions():吊销单个 / 全部会话。
仓库实战:从fetchOptions到"双通道"令牌机制
Better Auth 默认使用 Cookie 承载会话,但这在纯 API/移动端或需要显式携带访问令牌的场景下并不够用。仓库客户端给出了一个完整的增强示例:
export const authClient = createAuthClient({ baseURL: BETTER_AUTH_API_URL, plugins: [organizationClient(), ssoClient()], fetchOptions: { credentials: 'include', auth: { type: 'Bearer', token: () => localStorage.getItem('better-auth-session-token') || '', }, onSuccess: (ctx) => { const authToken = ctx.response.headers.get('set-auth-token'); if (authToken) { localStorage.setItem('better-auth-session-token', authToken); } }, }, });要点拆解:
baseURL指向服务端挂载路径:BETTER_AUTH_API_URL实际为${baseURL}/v1/better-auth。这意味着服务端把 Better Auth 的默认basePath(/api/auth)收编到了自己 API 网关的/v1/better-auth前缀之下(见第十二节),客户端用同一 baseURL 对齐;credentials: 'include':携带跨域 Cookie(与 CORS 的credentials: true配套);- Bearer 令牌双通道:
signIn.email()成功后,登录组件把返回的data.token写入localStorage的better-auth-session-token(见 sign-in.tsx#L66-L70);所有后续请求通过fetchOptions.auth自动带上该令牌; - 令牌刷新回写:
onSuccess拦截响应头set-auth-token,一旦服务端签发新令牌就更新本地存储——这与服务端"会话滑动续期"(session.updateAge)机制前后呼应; - 登出时除了
authClient.signOut(),还需清理本地令牌:localStorage.removeItem('better-auth-session-token'),代码见 index.tsx#L85-L89。
此外,仓库还通过role-permissions.ts把 Better Auth 组织成员角色(MemberRoleEnum,来自@novu/shared)映射为权限集PermissionsEnum,用于 UI 层的Protect/has()权限门控——这是"认证(Better Auth)之上再叠一层业务级 RBAC"的常见模式。
十二、真实工程接线:Better Auth 如何挂在 /v1/better-auth
技能文档讲述的是 Better Auth 的通用集成规范,本仓库则展示了在大型单体 API 中安全接入 Better Auth的完整工程实践。
1. API 网关:认证路径与通用 body-parser 解耦
Better Auth 端点依赖原始请求体与流式处理,与 NestJS 默认的全局 JSON body-parser(bodyParser.json({ verify: rawBodyBuffer }))存在冲突。在 apps/api/src/bootstrap.ts#L134-L148 中,服务为认证路由做了专门的放行:
app.use((req, res, next) => { if (req.path.startsWith('/v1/better-auth')) { return next(); } return bodyParser.json({ verify: rawBodyBuffer })(req, res, next); }); app.use((req, res, next) => { if (req.path.startsWith('/v1/better-auth')) { return next(); } return bodyParser.urlencoded({ extended: true, verify: rawBodyBuffer })(req, res, next); });2. CORS:认证路由必须显式校验来源,禁止通配
由于认证涉及携带凭证的跨域 Cookie,CORS 规则必须特殊对待。在 apps/api/src/config/cors.config.ts#L47-L61 中:
function enableWildcard(req: Request): boolean { return ( (isDevelopmentEnvironment() || isWidgetRoute(req.url) || isInboxRoute(req.url) || isBlueprintRoute(req.url) || isWebChatRoute(req.url)) && !isBetterAuthRoute(req.url) ); } // BetterAuth routes require explicit origin validation for credential-based requests function isBetterAuthRoute(url: string): boolean { return url.startsWith('/v1/better-auth'); }即:开发环境或其他路由可以放宽为通配 origin,但/v1/better-auth路由始终走白名单精确校验(ALLOWED_ORIGINS_REGEX来自FRONT_BASE_URL),并且credentials: true。这正是技能文档中trustedOrigins/ Origin 校验在生产多域架构下的落地形态。
3. 特性开关:Clerk 与 Better Auth 双轨共存
本仓库的仪表盘处于从 Clerk 向 Better Auth 迁移的过渡期:VITE_EE_AUTH_PROVIDER用于选择'clerk'还是'better-auth',客户端同时实现了一套以ClerkProvider命名的兼容封装(其内部实际驱动的是authClient),并在window.Clerk上暴露session.getToken兼容层(见 apps/dashboard/src/utils/better-auth/index.tsx#L502-L510)。对应的服务端也在特性开关下条件注册 auth 模块——apps/api/src/app/auth/auth.module.ts#L2-L18 依据isClerkEnabled() || isBetterAuthEnabled()决定是否装配。启动配置在 apps/dashboard/src/config/index.ts#L7-L9 的EE_AUTH_PROVIDER读取逻辑、以及 apps/dashboard/src/config/index.ts#L42-L46 的BETTER_AUTH_BASE_URL(来自VITE_BETTER_AUTH_BASE_URL,回退到VITE_API_HOSTNAME、最终默认http://localhost:3000)中可以看到完整取值链。
这一整套接线完整对应了技能文档第三节baseURL(仅在环境变量未设置时才显式配置)与basePath的思想,可作为在既有网关/NestJS/多环境体系内引入 Better Auth 的范本。
十三、类型安全:让认证类型贯穿全栈
Better Auth 的另一个卖点是端到端类型推断。
1. 服务端推断
从服务端auth实例直接推导会话与用户类型:
type Session = typeof auth.$Infer.Session; // 整个会话(user + session) type SessionUser = typeof auth.$Infer.Session.user; // 仅用户部分2. 前后端分离的跨项目推断
当客户端与服务端位于不同的项目/代码库时,无法直接import服务端auth实例。此时用泛型把类型"传递"过去:
// 服务端:auth 实例推导出的类型被引用进客户端项目的 .d.ts 或共享类型文件 createAuthClient<typeof auth>();通过createAuthClient<typeof auth>(),客户端方法签名、插件方法返回值与useSession()的数据结构都将和服务端完全对齐,杜绝手工维护接口类型导致的漂移。
3. 仓库中的实践佐证
本仓库也遵循"先推断、后导出"的类型哲学:客户端创建后直接导出export type AuthClient = typeof authClient;(见 client.ts#L28),供组件与工具函数引用;同时自定义的会话用户、组织类型(BetterAuthUser、BetterAuthOrganization,见 auth-context.ts#L4-L16)通过类型定义把 Better Auth 数据显式收敛到业务层可用的形态。
十四、常见陷阱清单(速查)
技能文档在末尾汇总了 6 条高频坑,本文按"症状 → 规避"展开:
| # | 陷阱 | 规避要点 |
|---|---|---|
| 1 | 模型名 vs 表名混淆 | 配置中永远用 ORM 模型名(如 Prisma 引用名user),而不是数据库表名(如users) |
| 2 | 插件 schema 过期 | 每次新增/变更插件后必须重跑 CLI(generate/migrate),让表结构与类型同步 |
| 3 | secondaryStorage 的"默认抢占" | 一旦定义secondaryStorage,会话默认写入它而非数据库;需要双写时显式开session.storeSessionInDatabase: true |
| 4 | Cookie 缓存的自定义字段缺失 | 自定义会话字段不会被写进 cookieCache,读取时总会回到存储重新拉取;不要假设缓存里有全部字段 |
| 5 | 无状态模式的登出语义 | 无数据库时会话只存在于 Cookie,登出 = 等 Cookie 到期/主动失效(可借助cookieCache.version强制全体失效) |
| 6 | 改邮箱流程的方向 | 更换邮箱时会先给当前邮箱发送确认,成功后再向新邮箱发送验证——两端邮件逻辑都要实现 |
总结
Better Auth 的价值在于把"认证"从散落的重复劳动收敛为一套类型安全的配置 + 可组合插件体系。其工程要点可概括为四句话:
- 环境变量优先:
BETTER_AUTH_SECRET与BETTER_AUTH_URL是第一公民,配置对象只是兜底; - 一切皆模型:数据库连接与钩子都围绕 ORM 模型名工作,不要被物理表名带偏;
- 会话是策略问题:DB / secondary storage / 无状态 Cookie 三种模式按规模与形态选择,Cookie 缓存选型要权衡体积、可读性与安全;
- 插件要按路径导入、客户端要配对挂载:schema、端点、客户端方法三者同生共灭。
如果你要在一套已有网关、多环境、多租户的工程里落地它,本仓库在apps/api(/v1/better-auth前缀 + body-parser 放行 + CORS 白名单)与apps/dashboard(createAuthClient插件组合 + Bearer 双通道令牌 + organization/sso 客户端方法)两侧的接线代码,是一份可直接参照的生产级样本;而本文所述的完整配置语义、CLI 生命周期、钩子上下文与陷阱清单,则来自仓库技能文档.cursor/skills/better-auth-best-practices/SKILL.md,可作为随时查阅的速查手册。
【免费下载链接】novuThe open-source communication infrastructure for agents and products项目地址: https://gitcode.com/GitHub_Trending/no/novu
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考