ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AIRI 认证契约共享层:解读 @proj-airi/auth-shared 的 Schema、Session 与授权策略设计

AIRI 认证契约共享层:解读 @proj-airi/auth-shared 的 Schema、Session 与授权策略设计 AIRI 认证契约共享层解读 proj-airi/auth-shared 的 Schema、Session 与授权策略设计【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi导读proj-airi/auth-shared是 Project AIRI 服务端认证体系的中立契约层它同时被资源 APIserver/apps/api与独立认证服务server/apps/auth依赖承载 Better Auth 所拥有的 PostgreSQL 表结构、服务端进程间交换的认证主体principal形状以及必须在两个进程中保持完全一致的授权策略如封禁过期判定。读完本文你将理解 AIRI 为什么要把认证契约拆成独立包、8 张共享数据表的字段设计含义、AuthSession与isUserBannedNow的用法以及无运行时依赖约束如何让两个应用协议共享、互不依赖。一、为什么需要独立的认证契约包在 server/packages/auth-shared/README.md 中该包被明确定位为 Neutral authentication contracts shared by the resource API and the standalone auth service即资源 API 与独立认证服务之间共享的中立认证契约。在 AIRI 的架构中认证职责被拆成了两个独立进程认证服务Auth Server运行 Better Auth负责会话、社交登录、Magic Link、密码与 OIDC 流程对应server/apps/auth资源 APIResource API提供产品业务接口自身不实例化 Better Auth只负责校验来访身份对应server/apps/api。如果认证协议只存在于其中一个应用内另一方就必须直接导入对方模块形成强耦合。auth-shared包的存在正是为了消除这种耦合两个应用都依赖同一个轻量协议包而不是互相依赖lets both applications depend on the same protocol without depending on each other。从 server/apps/auth/README.md 可以进一步看到分工认证表与主体契约放在proj-airi/auth-sharedDrizzle 在 API 启动时读取共享迁移文件API 是共享数据库迁移的拥有者The API remains the migration owner而 Auth 服务不负责在正常启动过程中执行迁移。二、包的边界该做什么不该做什么原文档用两个清单明确了包的职责边界这是整个设计最重要的部分用于Use it forBetter Auth 拥有的 PostgreSQL schema认证相关的数据表定义服务端代码内交换的认证主体/会话形状即AuthSession类型契约必须在两个进程中保持一致的授权策略例如封禁过期处理ban expiry handling。不用于Do not use it forBetter Auth 运行时构造或 HTTP 路由这些属于server/apps/auth的运行时职责服务环境解析、数据库连接池、Redis、邮件或遥测基础设施细节一律不进共享包导入server/apps/api或server/apps/auth共享包必须保持零反向依赖。这一约束在 package.json 中得到印证整个包的 dependencies 只有drizzle-orm一项没有 better-auth、没有服务端框架、没有环境解析库——keeping this package free of runtime composition保持该包不包含运行时组合是它存在的根本理由。三、Better Auth 拥有的 PostgreSQL Schemaschema.ts 使用drizzle-orm/pg-core定义了 8 张表与完整的 Drizzle relations构成了 Better Auth 的持久化层。入口 index.ts 以export * from ./schema和export * from ./session对外暴露全部契约。3.1 核心身份表user / session / account / verificationuser表以text主键id包含name非空、email非空且唯一、emailVerified默认false、image可空。值得注意的是用户表上额外挂了账户封禁字段banned: boolean默认falsebanReason: textbanExpires: timestamplastSeenAt: timestamp。源码注释明确了这些字段的所有权账户封禁字段由私有管理后台private management backend拥有banGuard会在会话创建时拒绝被封禁用户而资源与 userinfo 路由会对已签发的 OIDC access token 重新检查这些字段。session表主键id关键字段为expiresAt非空、token非空且唯一、ipAddress、userAgent、userId非空外键级联删除引用user.id。表级定义了session_userId_idx与session_expires_at_idx两个索引分别加速按用户查会话和按过期时间清理会话。account表承载 OAuth/社交账号绑定字段包含accountId、providerId、accessToken、refreshToken、idToken、accessTokenExpiresAt、refreshTokenExpiresAt、scope、password密码登录场景等。定义了两个复合索引account_userId_idx与account_account_id_provider_id_idx按accountId providerId联合索引正是按第三方账号反查本地用户的典型查询路径。verification表通用的验证码/一次性凭证存储字段为identifier、value、expiresAt配有verification_identifier_idx索引。它是 Better Auth 实现 Magic Link 等流程的底层存储。所有表都遵循同一套时间戳约定createdAt默认defaultNow()updatedAt默认defaultNow()且通过$onUpdate(() new Date())在更新时自动刷新——这在 schema.ts 中逐表可见。3.2 OIDC 扩展表jwks / oauth_client / oauth_refresh_token / oauth_access_token / oauth_consentAIRI 在 Better Auth 基础之上叠加了 OIDC 能力因此共享 schema 额外定义了 5 张 OAuth/OIDC 相关表jwks保存 JWT 签名密钥对字段为publicKey、privateKey、createdAt、expiresAt。资源 API 正是通过它暴露的 JWKS 端点做无状态 JWT 验签oauth_clientOIDC 客户端注册信息字段极为丰富clientId唯一、clientSecret、disabled、skipConsent、enableEndSession、subjectType、scopestext数组、redirectUris非空数组、postLogoutRedirectUris、tokenEndpointAuthMethod、grantTypes、responseTypes、public、requirePKCE、metadatajsonb等oauth_refresh_token刷新令牌表token、clientId、sessionId删除会话时置空onDelete: set null、userId级联删除、expiresAt、revoked吊销时间、authTime、scopes并建了 token / userId / sessionId / clientId 四个索引oauth_access_token访问令牌表token唯一通过refreshId关联刷新令牌级联删除sessionId删除时置空scopes非空数组oauth_consent用户授权记录记录clientId userId下的授权scopes用于 OIDC 授权码流程的同意管理。3.3 关系relations定义schema 底部通过 Drizzle 的relations()定义了完整的实体关系图userRelations一个用户拥有多个sessions、accounts、oauthClients、oauthRefreshTokens、oauthAccessTokens、oauthConsentssessionRelations会话归属一个用户且可关联多个刷新令牌与访问令牌oauthClientRelations、oauthRefreshTokenRelations、oauthAccessTokenRelations、oauthConsentRelations则从客户端、令牌、授权三个角度补全了外键语义。这些 relations 让资源 API 可以通过 Drizzle 的db.query做类型安全的关联查询如 request-auth.ts 中的db.query.user.findFirst无需手写 join 条件。四、认证主体契约AuthSessionsession.ts 定义了服务端代码内交换的认证主体形状AuthSession注释明确其用途是 Authenticated principal exposed to AIRI resource handlers暴露给 AIRI 资源处理器的已认证主体。export interface AuthSession { user: { id: string name: string email: string emailVerified: boolean image?: string | null banned?: boolean | null banReason?: string | null banExpires?: Date | null lastSeenAt?: Date | null createdAt: Date updatedAt: Date } session: { id: string token: string userId: string expiresAt: Date createdAt: Date updatedAt: Date ipAddress?: string | null userAgent?: string | null } }它的关键设计点是主体user与凭证会话session分开建模字段与user/session表一一对应属于持久化层的安全投影——资源处理器永远通过AuthSession拿到完整上下文而不需要直接访问数据库或 Better Auth 内部适配器封禁字段直接内联在 user 对象上banned、banReason、banExpires保证资源处理器在每次请求时都能做授权判断所有字段均为可序列化的基础类型string、boolean、Date、null使其既能作为进程内类型也能作为跨服务如 JWT 载荷映射、测试夹具的传输形状。在 request-auth.ts 中可以看到资源 API 直接复用该类型export type RequestAuthSession AuthSession整个资源侧的认证结果就是这一个共享类型。五、共享授权策略isUserBannedNowAuthSession解决主体长什么样的问题isUserBannedNow则解决这个主体现在是否被允许的问题它是文档强调的授权策略在两个进程中保持一致的直接载体export function isUserBannedNow(user: { banned?: boolean | null, banExpires?: Date | string | null }): boolean { if (!user.banned) return false if (user.banExpires null) return true return new Date(user.banExpires).getTime() Date.now() }判定逻辑session.ts未标记banned或为false/null→ 返回false未封禁已标记banned但banExpires为空 → 返回true永久封禁已标记banned且有过期时间 → 比较过期时间与当前时间过期的临时封禁视为不生效Expired temporary bans are treated as inactive on stateless JWT paths即封禁到期后自动放行无需人工解封。该函数接受banExpires为Date | string兼容从数据库读出的Date与从 JWT 载荷映射出的字符串时间两种形态且不依赖 Better Auth 运行时这正是在没有 Better Auth 运行时的情况下评估其持久化封禁字段Evaluates Better Auths persisted ban fields without requiring its runtime的实现。5.1 在认证侧的应用banGuard 插件server/apps/auth/src/plugins/ban-guard.ts 实现了 Better Auth 插件banGuard()它通过schema.user.fields声明banned、banReason、banExpires三个只读字段input: false业务侧不可写入只能由管理后台写入并在databaseHooks.session.create.before钩子中于Better Auth 每次创建新会话时读取用户并调用共享的isUserBannedNowconst user await context.context.internalAdapter.findUserById(session.userId) as BanState | null if (!isUserBannedNow(user ?? {})) return throw APIError.from(FORBIDDEN, { code: BANNED_USER, message: This account has been banned, })被封禁用户会被拒绝创建新会话插件注释还特别说明它不会清除已过期的封禁因为并发的管理请求可能在会话钩子读取之后续期封禁It does not clear expired bans because a concurrent management request can renew a ban after the session hook reads it——这正是共享契约需要精确、幂等的原因。5.2 在资源侧的应用每次请求重校验server/apps/api/src/libs/request-auth.ts 展示了共享契约在资源 API 侧的完整调用链。其核心流程resolveRequestAuth从Authorization: Bearer token头读取访问令牌request-auth.ts优先匹配测试令牌TEST_AUTH_TOKEN使用timingSafeEqual做常数时间比较构造一个AuthSession形状的测试会话见 request-auth.ts否则走resolveJWTAccessToken通过createRemoteJWKSet拉取认证服务的/api/auth/jwksURL 由AUTH_SERVER_INTERNAL_URL ?? AUTH_SERVER_URL决定带缓存用jose的jwtVerify做本地签名验签——no database query for the token itself但仍需要一次findUserById来构造完整的RequestAuthSessionrequest-auth.ts验签通过后对resolved.user再次调用isUserBannedNow命中则直接返回null拒绝请求。第 4 步是文档所说ban expiry handling 必须双端一致的精华所在OIDC JWT access token 是无状态的——只验签、不查会话行因此登录时session.create.before钩子无法在令牌 TTL 内使其失效。资源 API 在每次请求时顺手free: the user row is already loaded重新检查user.banned字段才能让封禁在 HTTP、WebSocket、OIDC 令牌三条路径上即时生效。整个函数不实例化 Better Auth、不依赖其内部适配器仅消费共享契约与包的设计目标完全一致。六、测试验证与工程约束共享契约的正确性有测试兜底server/apps/api/src/schemas/auth-schema-contract.test.ts 对认证 schema 契约做断言server/apps/api/src/libs/tests/request-auth.test.ts 验证资源侧resolveRequestAuth对isUserBannedNow的封禁重校验行为server/apps/auth/src/tests/routes-userinfo.test.ts 覆盖认证侧 userinfo 路由对封禁主体/过期封禁的处理。工程约束方面proj-airi/auth-shared是私有包private: true依赖仅有drizzle-orm提供typecheck脚本exports直接指向./src/index.ts源码直出无构建步骤保持轻量。消费方server/apps/api/package.json、server/apps/auth/package.json通过 workspace 引用它在 server/apps/auth/Dockerfile 等构建产物中server/packages/auth-shared也被显式复制进构建上下文说明它是两个服务部署时的公共依赖。七、总结协议共享、运行时隔离proj-airi/auth-shared的价值可以归结为三个层面单一事实来源Single Source of TruthPostgreSQL schema、AuthSession形状、封禁判定函数只在一处定义Auth 服务与资源 API 同时引用杜绝两套认证逻辑漂移运行时隔离包内不含 Better Auth 运行时、路由、环境解析、连接池或基础设施依赖两个应用可以安全地共同依赖它而彼此零引用授权策略双端一致登录入口用banGuard拦截新会话资源入口用isUserBannedNow兜底无状态 JWT共享函数保证了无论封禁是永久还是限期、无论用户从哪条路径进入判定结果都严格一致。对于任何认证服务与业务服务分离的架构这个包的边界设计——契约共享、实现私有、策略一致、迁移归属明确——都是一份值得参考的模板。想深入探索的读者建议从 schema.ts 的表结构开始再对照 ban-guard.ts 与 request-auth.ts 两个消费方的调用链即可完整串起 AIRI 的认证数据流。【免费下载链接】airi Self hosted, you-owned Grok Companion, a container of souls of waifu, cyber livings to bring them into our worlds, wishing to achieve Neuro-samas altitude. Capable of realtime voice chat, Minecraft, Factorio playing. Web / macOS / Windows supported.项目地址: https://gitcode.com/GitHub_Trending/ai/airi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表