认证与管理架构
深入说明本模板中的 Better Auth 会话机制、OAuth、RBAC,以及积分/配额等业务数据建模方式。
认证与 Admin
本文是本模板中认证与管理员授权机制的核心说明文档。
本文包含:
- 本项目实际使用的 Better Auth 模型
- 邮箱密码与 Google OAuth 如何建立并维持会话
- 为什么要拆分
users、accounts、sessions、verifications - 什么是认证数据,什么是业务数据(积分、配额、权限)
- 本代码库中 Admin RBAC 的实现方式
1) 本模板使用的认证模型
本项目使用基于 Cookie 的服务端会话,并以 Postgres 作为会话真相源。 默认情况下它不是 JWT-first 模型。
原因:
src/lib/auth.ts中使用 Better Auth + Drizzle + Postgres 适配器- 会话存储映射到
src/db/schema.ts里的sessions表 - 启用了
nextCookies()插件 - 没有启用以 JWT 作为主认证机制的插件
实际含义:
- 浏览器保存一个 HttpOnly 认证 Cookie(会话 token)
- 服务端用该 token 去
sessions表校验 - 根据会话记录定位用户身份
Better Auth 支持 JWT,但它是可选能力,不会替代这里正在使用的 DB 会话模型。
2) 核心认证表与职责
Better Auth 有意将身份、登录方式、登录状态分离。
users(身份档案)
表示这个人是谁。
常见字段:
- 资料信息(
nickname,avatar_url) - 角色(
role) - 验证/状态字段
- 创建与更新时间
在本模板中还映射了:
uuid(创建时生成)role
users 表应尽量保持稳定,避免高频业务写入。
accounts(认证方式)
表示这个人如何登录。
- 一个用户可以绑定多个登录提供方
- 保存提供方身份信息和提供方 token
常见字段:
provider_idaccount_id- 密码哈希(邮箱密码登录)
- OAuth access token / refresh token
- OAuth token 过期时间
这张表以凭据为核心,安全敏感度高。
sessions(登录状态)
表示当前在你系统中的登录会话。
常见字段:
user_idtokenexpires_atip_addressuser_agent
特点:
- 会话过期时间决定用户是否仍登录
- 撤销会话通常通过删除/失效会话记录
- 滑动续期由会话策略控制
verifications(一次性验证流)
用于临时 token 场景,例如:
- 重置密码
- 邮箱验证
- 魔法链接 / OTP(启用时)
普通登录/退出不会持续写入这张表,所以行数很少是正常现象。
3) 本代码库中的端到端认证流程
3.1 邮箱 + 密码注册
- 客户端提交注册表单
- Better Auth 创建
users记录 src/lib/auth.ts中databaseHooks.user.create.before确保有uuid- Better Auth 创建邮箱密码对应的
accounts记录 - Better Auth 创建
sessions记录 - 在响应中写入会话 Cookie
databaseHooks.user.create.after异步发送欢迎邮件
结果:用户注册后立即处于登录状态。
3.2 邮箱 + 密码登录
- 客户端提交凭据
- Better Auth 校验
accounts中密码哈希 - Better Auth 创建/刷新
sessions记录 - 设置/更新会话 Cookie
3.3 Google OAuth 登录
- 用户点击 Google 登录
- 执行 OAuth 跳转与回调流程
- Google 返回账号身份(
sub)及资料 claims - Better Auth 查找或创建用户
- Better Auth 创建/更新
google的accounts记录 - Better Auth 创建会话记录
- 写入会话 Cookie
4) OAuth 身份识别的重要规则
Google 登录不要求必须是 Gmail 邮箱。
- 用户可以使用 Gmail、Outlook、Yahoo 或自定义域邮箱(只要绑定了 Google 账号)
- 身份应基于提供方账号标识(
provider_id,account_id/sub),不是邮箱域名
不要把邮箱域名当作提供方身份证明。
5) 会话过期 与 OAuth Token 过期
这是两个独立生命周期。
应用会话生命周期(sessions)
- 决定用户是否已登录你的应用
- 会话过期后,
getSession()返回 null - 用户必须重新登录
提供方 token 生命周期(accounts)
- 决定后端是否还能调用提供方 API
- access token 通常较短期
- refresh token 可用于换新 access token
关键点:
- Better Auth 会话过期不会自动用 Google refresh token 重新登录
- refresh token 用于调用提供方 API,不用于应用登录认证
6) 为什么会话与 OAuth Token 必须分离
这种分离是为了可预测的安全边界:
- OAuth refresh token 不是应用身份凭证
- 会话撤销必须真实有效
- 用户应能被要求显式重新认证
- 安全与审计边界更清晰
如果用 refresh token 自动重建应用会话,会削弱撤销能力和审计可追溯性。
7) 本模板中的授权(RBAC)
认证回答“你是谁”,授权回答“你能做什么”。
角色来自 DB 的 users.role:
useradmin_roadmin_rw
角色校验在 src/lib/authz.ts:
requireAdminRead()-> 允许admin_ro与admin_rwrequireAdminWrite()-> 仅允许admin_rw
Admin 布局守卫:src/app/(admin)/layout.tsx。
8) 当前 Admin 页面与 API
Admin 页面
- 仪表盘:
src/app/(admin)/admin/page.tsx - 用户列表:
src/app/(admin)/admin/users/page.tsx - 用户详情/配额/积分:
src/app/(admin)/admin/users/[uuid]/page.tsx - 订单列表/状态:
src/app/(admin)/admin/orders/page.tsx - 反馈:
src/app/(admin)/admin/feedbacks/page.tsx - 预约:
src/app/(admin)/admin/reservations/page.tsx - 分销:
src/app/(admin)/admin/affiliates/page.tsx
Admin API 路由
GET /api/admin/users-> 分页用户列表GET /api/admin/users/[uuid]-> 用户详情 + 月度使用 + 积分汇总PATCH /api/admin/users/[uuid]-> 更新monthlyCreditsQuotaGET /api/admin/users/[uuid]/credits-> 积分汇总 + 台账POST /api/admin/credits/grant-> 发放积分POST /api/admin/credits/adjust-> 正负积分调整GET /api/admin/orders-> 按状态过滤分页订单(all|paid|created|deleted)
9) Cookie Session 与 JWT(你现在实际是什么)
当前配置:
- 会话 Cookie 保存 session token
- 服务端拿 token 去
sessions表校验 - 用户身份由 DB 会话解析
所以这是 DB 支撑的会话认证,不是纯无状态 JWT 认证。
纯 JWT 模型(当前默认不是):
- token 本身携带 claims
- 每次请求做签名校验
- 基础校验通常不需要查 DB
10) 为什么仍然需要 BETTER_AUTH_SECRET
即使 session token 是随机串,也仍然需要 secret 去完成签名/验签等密码学操作。
Better Auth 的 secret 解析顺序:
- 配置里显式
secret BETTER_AUTH_SECRETAUTH_SECRET- 内置 fallback(开发方便,不适合生产)
所以没有环境变量时应用可能还能跑,但这不是安全的生产配置。
请为每个环境设置强随机且稳定的 secret。 变更 secret 可能导致已签名认证数据失效并引发用户退出登录。
11) 业务数据建模:哪些不该放在认证表里
不要把高频变化的业务计数塞进认证表。
认证表应保持“只做认证”
users: 身份/资料/角色accounts: 登录提供方 + 凭据/tokensessions: 登录状态verifications: 一次性验证 token
产品/计费状态应放在领域表
推荐模式:
- 当前状态表(快速读取)
- 追加写 ledger/event(审计/重算)
- entitlement 表(功能权限)
示例结构:
-
user_balanceuser_id(PK)credits_balancequota_limitquota_usedplan_idbilling_period_startbilling_period_endupdated_at
-
usage_eventsiduser_idtypeamountcost_creditsmetadata(JSON)created_at
-
entitlementsuser_idkeyvaluesource(plan/admin/promo)expires_at
这样可以让认证层稳定,同时业务逻辑可审计、可回放。
12) 配额/积分消费与事务安全
消耗配额或积分时:
- 必须使用 DB 事务
- 先检查余额/可用配额
- 写入 usage ledger 事件
- 有条件地更新余额
这能避免并发下的竞态和负余额问题。
在本模板中:
- 任务使用量记录在
tasks - 积分台账记录在
credits - 月度配额(
users.monthly_credits_quota)会在扣积分前校验
13) 面向未来:Organizations
如果未来要支持团队:
- 新增
organizations - 把余额/配额/entitlement 下沉到组织级
- 个人账号视为单人组织
尽早按这个方向设计,可以避免后期大规模重构。
14) 环境变量与初始化清单
最小认证相关环境变量:
BETTER_AUTH_URLNEXT_PUBLIC_AUTH_BASE_URLBETTER_AUTH_SECRET(强随机)GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET(启用 Google 时)
如果 schema 变更,执行迁移:
pnpm drizzle-kit generate --config src/db/config.ts
pnpm drizzle-kit migrate --config src/db/config.ts最终总结
- 本项目是 Cookie + DB Session(不是 JWT-first)
- OAuth 身份以 provider account 为准,不以邮箱域名为准
- 应用会话生命周期与 provider token 生命周期相互独立
- 认证表应保持 auth-only
- 积分/配额/权限应放在带 ledger 的业务领域表
- Admin RBAC 基于 DB 角色并在服务端严格保护
该架构能提供清晰的安全边界、可运维性,以及后续扩展能力。