ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

React Starter Kit 认证体系全解:基于 Better Auth 的多认证方式与多租户架构

React Starter Kit 认证体系全解:基于 Better Auth 的多认证方式与多租户架构 React Starter Kit 认证体系全解基于 Better Auth 的多认证方式与多租户架构【免费下载链接】react-starter-kitModern React starter kit with Bun, TypeScript, Tailwind CSS, tRPC, Stripe, and Cloudflare Workers. Production-ready monorepo for building fast web apps.项目地址: https://gitcode.com/gh_mirrors/rea/react-starter-kit本指南以 docs/auth/index.md 为核心骨架结合仓库源码apps/api/lib/auth.ts、apps/app/lib/auth.ts、db/schema/等深入展开帮助读者完整理解该 Starter Kit 的认证架构、插件体系、数据模型、配置方式与边界路由机制。导读本文是 React Starter Kit 认证模块的技术全景指南。该项目使用 TypeScript 原生的认证框架 Better Auth认证逻辑完全运行在 API WorkerCloudflare Workers内部开箱即用地集成了邮箱 OTP、邮箱密码、Google OAuth、PasskeyWebAuthn、匿名会话五种登录方式并在此基础上叠加了基于组织的多租户multi-tenancy与 Stripe 订阅计费。读完本文你将掌握认证实例的服务端/客户端配置方式、插件启用规则、9 张认证数据表的字段与关系、前缀 ID 生成机制以及认证提示 Cookie如何驱动边缘路由实现登录用户进 App、匿名访客进营销页的动静分离。认证架构概览Better Auth 全栈运行于 API Worker项目的认证能力完全由 Better Auth 将原始请求转交给auth.handler处理app.on([GET, POST], /api/auth/*, (c) { const auth c.get(auth); if (!auth) { return c.json({ error: Authentication service not initialized }, 503); } return auth.handler(c.req.raw); });tRPC 的请求上下文则通过auth.api.getSession({ headers: req.headers })解析当前会话将session与user注入每个 tRPC procedure见 apps/api/lib/app.ts。这意味着 API 层天然先认证、后执行业务protectedProcedure通过session/user非空来收窄类型见 apps/api/lib/context.ts。支持的认证方式所有认证方法产出同一种会话格式用户可以在一个账号下同时关联多种登录方式例如既绑定邮箱 OTP又注册了 Passkey。内置能力如下方式说明邮箱 OTP通过邮件发送 6 位无密码验证码邮箱密码服务端已启用并支持密码重置邮件Starter UI 未提供密码表单Google OAuth可选重定向流程的社交登录PasskeysWebAuthn 生物识别 / 安全密钥匿名服务端/客户端已具备访客会话能力Starter UI 未提供访客会话控制插件体系服务端与客户端必须成对启用Better Auth 的功能通过插件扩展。服务端与客户端必须启用相互匹配的插件否则两端能力不一致会导致运行时问题。下表是项目实际启用的插件清单插件服务端客户端用途emailOTPemailOTP()emailOTPClient()无密码 OTP 登录organizationorganization()organizationClient()多租户组织与角色passkeypasskey()passkeyClient()WebAuthn 认证anonymousanonymous()anonymousClient()访客会话stripestripe()stripeClient()订阅计费服务端配置详解认证实例在 apps/api/lib/auth.ts 中按请求创建per-request其核心配置如下export function createAuth(db: Database, env: AuthEnv): Auth { return betterAuth({ baseURL: ${env.APP_ORIGIN}/api/auth, trustedOrigins: [env.APP_ORIGIN], secret: env.BETTER_AUTH_SECRET, database: drizzleAdapter(db, { provider: pg, schema: { ... } }), emailAndPassword: { enabled: true, sendResetPassword: async ({ user, url }) { await sendPasswordReset(env, { user, url }); }, }, emailVerification: { sendVerificationEmail: async ({ user, url }) { await sendVerificationEmail(env, { user, url }); }, }, // 两个凭证都未设置时返回空对象Google 保持关闭 // 只设置其中一个会抛错——见 googleProvider() 的实现。 socialProviders: googleProvider(env), plugins: [ anonymous(), organization({ allowUserToCreateOrganization: true, organizationLimit: 5, creatorRole: owner, }), passkey({ rpID, rpName: env.APP_NAME, origin: env.APP_ORIGIN }), emailOTP({ otpLength: 6, expiresIn: 300, allowedAttempts: 3 }), ...stripePlugin(db, env), ], }); }几个值得深挖的配置要点类型安全的数据库连接Database是PostgresJsDatabaseDatabaseSchema的本地别名。Better Auth 自带的DB类型是{ [key: string]: any }会让该文件内所有查询失去类型检查因此项目显式传入带完整 schema 类型的 Drizzle 客户端见 apps/api/lib/auth.ts。account模型重命名为identityaccount: { modelName: identity }让表名更准确地表达其职责——它存放所有认证身份无论 OAuth 还是密码凭证见 apps/api/lib/auth.ts。对应的 Drizzle 表同样命名为identity见 db/schema/user.ts。组织插件的默认策略允许用户自行创建组织allowUserToCreateOrganization: true、每人上限 5 个organizationLimit: 5、创建者默认角色为ownercreatorRole: owner。Passkey 参数rpID取自APP_ORIGIN的 hostname生产为域名、开发为 localhostrpName为APP_NAMEorigin为APP_ORIGIN无尾部斜杠。OTP 参数验证码长度 6 位、有效期 300 秒5 分钟、最多尝试 3 次。会话初始化钩子通过databaseHooks.session.create.before在创建新会话时自动选中最早的成员资格作为activeOrganizationId并以id打破createdAt平局保证多次登录的选择结果稳定见 apps/api/lib/auth.ts 与 apps/api/lib/auth.ts。条件加载Google OAuth 与 Stripe 插件的全有或全无策略项目对可选能力采用全部配置或全部不配置绝不半配置的防御性策略Google OAuthgoogleProvider()见 apps/api/lib/auth.tsGOOGLE_CLIENT_ID与GOOGLE_CLIENT_SECRET均未设置时返回{}Google 保持关闭只设置其中一个时直接throw让错误的部署立刻失败而不是表现为登录按钮莫名消失。Stripe 计费stripePlugin()见 apps/api/lib/auth.ts仅在STRIPE_SECRET_KEY、STRIPE_WEBHOOK_SECRET、STRIPE_STARTER_PRICE_ID、STRIPE_PRO_PRICE_ID四个变量全部设置时才激活。全部未设置时应用正常工作受保护的billing.subscription查询会报告集成未启用Stripe 变更类端点返回 404部分设置时createAuth抛错明确指出缺失的变量名。STRIPE_PRO_ANNUAL_PRICE_ID在任何情况下都可选。此外还有一个被广泛使用的派生函数configuredSocialProviders(env)见 apps/api/lib/auth.ts它从配置 Google 的同一个调用推导出当前部署实际可用的社交提供商列表SPA 据此只渲染可用的按钮——按钮与凭证永远不可能不一致避免了另存一份客户端标志导致两端漂移的问题。ID 生成带前缀的 CUID2所有认证表的 ID 都在应用层生成使用带语义前缀的 CUID2见 db/schema/id.tsadvanced: { database: { generateId: ({ model }) generateAuthId(model), }, },前缀与 Better Auth 内部模型名一一对应模型名前缀表userusrusersessionsessessionaccountidnidentity经account.modelName映射verificationvfyverificationorganizationorgorganizationmembermemmemberinvitationinvinvitationpasskeypkypasskeysubscriptionsubsubscription生成的 ID 形如usr_cm...、ses_cm...、org_cm...前缀 16 位 CUID2 主体一眼即可识别记录类型。未知模型名会抛出明确错误非认证表的通用生成函数generateId(prefix)强制要求 3 位小写字母前缀。ID 设计理由详见 docs/specs/prefixed-ids.md。客户端配置详解认证客户端位于 apps/app/lib/auth.tsexport const auth createAuthClient({ baseURL: baseURL authConfig.api.basePath, plugins: [ anonymousClient(), emailOTPClient(), organizationClient(), passkeyClient(), stripeClient({ subscription: true }), ], });baseURL在浏览器环境下取自window.location.originSSR/构建期回退到http://localhost:5173再拼接authConfig.api.basePath即/api/auth见 apps/app/lib/auth-config.ts。stripeClient({ subscription: true })启用订阅相关客户端能力。该模块还导出了从auth.$Infer.Session推导的User与Session类型包含插件扩展的字段见 apps/app/lib/auth.ts。重要约定不要直接使用auth.useSession()。会话状态完全由 TanStack Query 统一管理以保证缓存与一致性参见 Sessions Protected Routes 与 apps/app/lib/queries/session.ts。客户端错误文案与重定向 URL 的安全校验仅允许同源相对路径拒绝//开头的协议相对 URL集中在 apps/app/lib/auth-config.ts。认证路由一览Better Auth 在/api/auth/*下暴露 HTTP 端点与 tRPC 一同挂在 Hono 应用上。项目中实际使用的路由分组如下/api/auth/sign-in/* Sign-in 端点邮箱、社交、passkey /api/auth/sign-up/* Sign-up 端点 /api/auth/sign-out 会话终止 /api/auth/get-session 当前会话数据 /api/auth/callback/* OAuth 回调 /api/auth/email-otp/* OTP 发送与校验 /api/auth/passkey/* WebAuthn 注册与认证 /api/auth/organization/* 组织 CRUD 与成员管理完整端点清单以 Better Auth 官方 API Reference 为准项目在 apps/api/lib/app.ts 的/api信息端点中同样标注了auth: /api/auth。数据库表9 张表支撑全部认证能力认证共使用 9 张数据库表定义于 db/schema/ 下表文件说明userdb/schema/user.ts用户账户与资料信息含isAnonymous、stripeCustomerIdsessiondb/schema/user.ts活跃会话含activeOrganizationIdidentitydb/schema/user.ts认证身份OAuth 与密码凭证Better Auth 的account模型verificationdb/schema/user.ts邮箱验证与 OTP 令牌organizationdb/schema/organization.ts租户组织memberdb/schema/organization.ts组织成员关系与角色invitationdb/schema/invitation.ts待处理的组织邀请passkeydb/schema/passkey.tsWebAuthn 凭证存储subscriptiondb/schema/subscription.tsStripe 订阅状态表结构细节可以补充几点sessiontoken唯一userId级联删除对userId与activeOrganizationId建立索引db/schema/user.ts。identity(providerId, accountId)联合唯一约束确保同一提供商下账号不重复password字段存放密码凭证db/schema/user.ts。member(userId, organizationId)联合唯一role取值遵循 Better Auth 默认的owner/admin/member三角色模型db/schema/organization.ts。invitation默认每个邮箱每个组织一生一条邀请(organizationId, email)唯一约束acceptedAt/rejectedAt留给应用钩子使用——Better Auth 只会更新status不会填充这两个扩展字段db/schema/invitation.ts。passkey在 Better Auth 默认字段之外扩展了lastUsedAt安全审计、deviceName如 MacBook Pro与platformplatform/cross-platformdb/schema/passkey.ts。subscriptionreferenceId是多态外键——指向user.id或organization.id分别对应个人订阅与组织订阅db/schema/subscription.ts。Auth Hint Cookie驱动边缘路由的轻量信号API Worker 在登录时设置一个轻量 Cookie、登出时清除web 边缘 Worker 读取该 Cookie 决定/路由到哪已认证用户进入 App匿名访客进入营销页。实现见 apps/api/lib/auth.ts 的hooks.afterCookie 命名HTTPS 下为__Host-auth__Host-前缀强制要求SecureHTTP 开发环境为auth——浏览器会拒绝无 Secure 的__Host-Cookie。Cookie 属性path: /、secure: isSecure、httpOnly: true、sameSite: lax。生命周期在ctx.context.newSession存在时登录、注册、OAuth 回调设置值为1在/sign-out路径清除在/get-session且会话无效时过期、撤销、用户被删除清除陈旧 Cookie。需要特别强调的是这个 Cookie 只是路由提示不是安全边界。误报是可接受的代价仅是一次多出的重定向App 本身才是会话权威。完整设计决策见 ADR-001其中记录了两个被否决的备选方案——在 web 边缘直接校验会话耦合边缘与认证、增加延迟与故障点、直接读取 Better Auth 会话 Cookie对认证库内部格式过于脆弱。环境变量清单认证相关的环境变量及要求如下Zod 校验见 apps/api/lib/env.ts变量必填说明BETTER_AUTH_SECRET是用于签名会话与令牌的密钥Zod 要求长度至少 32 字符GOOGLE_CLIENT_ID否Google OAuth 客户端 ID——必须与 Secret 成对设置或都不设置GOOGLE_CLIENT_SECRET否Google OAuth 客户端密钥RESEND_API_KEY是发送 OTP 邮件的 Resend API KeyRESEND_EMAIL_FROM是认证邮件的发件地址Zod 校验为合法邮箱APP_NAME是显示名称用于邮件与 Passkey 提示APP_ORIGIN是完整源 URL如https://example.comZod 校验为合法 URLSTRIPE_SECRET_KEY否Stripe 密钥必须以sk_开头四个变量全设才启用计费STRIPE_WEBHOOK_SECRET否必须以whsec_开头STRIPE_STARTER_PRICE_ID/STRIPE_PRO_PRICE_ID否必须以price_开头STRIPE_PRO_ANNUAL_PRICE_ID否任何情况下均为可选邮件发送走 Resend开发环境下ENVIRONMENT development时OTP 验证码还会直接打印到控制台方便本地调试见 apps/api/lib/email.ts每次发送都新建 Resend 客户端避免 Worker 复用 isolate 时模块级客户端携带过期 envapps/api/lib/email.ts。进一步阅读各认证方式的深入指南邮箱 OTP、Passkeys、社交登录、组织多租户、会话与受保护路由数据库 schema 定义db/schema/index.ts 及 db/schema/user.ts 等各表文件计费集成apps/api/lib/auth.ts 中的stripePlugin、docs/billing/index.md前缀 ID 设计docs/specs/prefixed-ids.md认证提示 Cookie 决策docs/adr/001-auth-hint-cookie.md【免费下载链接】react-starter-kitModern React starter kit with Bun, TypeScript, Tailwind CSS, tRPC, Stripe, and Cloudflare Workers. Production-ready monorepo for building fast web apps.项目地址: https://gitcode.com/gh_mirrors/rea/react-starter-kit创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进