十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Civitai Monorepo:将 OAuth2/OIDC Provider 从主应用迁移至 apps/auth 的完整技术拆解

Civitai Monorepo:将 OAuth2/OIDC Provider 从主应用迁移至 apps/auth 的完整技术拆解 Civitai Monorepo:将 OAuth2/OIDC Provider 从主应用迁移至 apps/auth 的完整技术拆解【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本文基于仓库中的迁移规划文档 oauth-provider-to-auth-app.md详解 Civitai monorepo 如何把 OAuth2/OIDC Provider 从主 Next.js 应用整体搬入apps/authauth.civitai.com包括迁移之所以安全的底层前提共享 Postgres 统一的ApiKey哈希校验、SvelteKit 与 Next.js 的框架差异表、端点级迁移清单、Phase 0–7 的完整路线以及 GET 重定向与 POST 反向代理两种兼容策略的源码实现。读完你可以掌握同一 Postgres 作为信任集成点的跨应用令牌互认方案以及协议类服务迁移中重定向 vs 代理的取舍方法。1. 目标让 apps/auth 成为唯一身份权威规划文档的状态为plan / proposal2026-06-10关联文档见 centralized-auth-app.md§1c、§3——oauth/* 是 auth app 的天然租户、auth-verification-strategy.md 与 oauth-scoped-tokens.md。目标一句话概括让apps/authauth.civitai.com成为单一身份权威并包含它对第三方应用作为 OAuth2/OIDCProvider的角色。迁移前该 Provider 完全驻留在主 Next.js 应用里src/pages/login/oauth/*、src/pages/api/auth/oauth/*、src/server/oauth/*。迁移策略是把协议核心 同意consentUI搬进 hub主应用的旧路由降级为重定向用户页面与薄代理机器端点让已注册的第三方客户端零改动继续工作。2. 让这次迁移安全的关键事实文档把迁移的安全性归结为两个事实仓库源码可以逐一印证事实一两个应用连同一个 PostgresOAuth 令牌以哈希后的ApiKey行存储。主应用的 bearer-token 中间件对任何入站令牌先哈希、再到ApiKey表查找。只要 hub 用同一算法 同一密钥计算哈希generateSecretHashhub 签发的令牌就能在所有 spoke 上零改动通过校验。数据库就是集成点没有发明新的信任路径。事实二hub 已经拥有 RS256 签名密钥civitai/auth的maybeCreateSessionSigner/.well-known/jwks.jsonJWKS 端点。OIDCid_token的签名在 hub 里比在主应用里更顺理成章——discovery 文档的jwks_uri本来就指向 hub。对应到源码令牌哈希的实现位于共享包 secret-hash.ts// SHA-512 hash of a public key salted with NEXTAUTH_SECRET. Stored in the DB. export function generateSecretHash(key: string): string { const secret loadAuthEnv().NEXTAUTH_SECRET; if (!secret) { throw new Error([civitai/auth] NEXTAUTH_SECRET is required for generateSecretHash (token hashing)); } return createHash(sha512).update(${key}${secret}).digest(hex); }从该实现可以看到两个工程细节正好回应了文档中最高风险的假设密钥来源是包级 env 读取loadAuthEnv而非某个应用的私有 schema——源码注释明确写道hub 和主应用必须从同一个 key 推导出相同的 hash所以 salt 来自包 env 读取的NEXTAUTH_SECRET而不是 app 专属 schema。主应用从~/server/utils/key-generator再导出这两个函数保持旧调用点不变。这把密钥一致性从部署约定升级为代码层面的共享依赖。NEXTAUTH_SECRET缺失时快速失败fail fast注释解释若 salt 为undefined会计算出可预测的SHA512(key undefined)且与主应用的哈希不匹配从而无报错地破坏令牌校验——所以任何调用方在缺密钥时直接抛错。文档中的原始警示Phase 0 必须先验证的前提若generateSecretHash以NEXTAUTH_SECRET为 keyhub 必须共享它。若哈希不一致hub 签发的令牌会在主应用中静默校验失败。这是全项目风险最高的假设——先证明它。 从当前仓库看该 spike 的结论已落入共享包 civitai-auth 的实现中单一generateSecretHash实现 缺失即抛错。3. 框架差异为什么这不是复制粘贴主应用是React/Next Prisma tRPC NextAuthapps/auth是SvelteKit 2 / Svelte 5 Kysely 自研 RS256 会话无 NextAuth、无 Prisma、无 tRPC。规划文档给出的逐项对照表如下关注点主应用现状在apps/auth中OAuth 协议核心node-oauth/oauth2-server框架无关原样复用——在server.ts中构造其Request/Responseoauth/model.ts的 DB 访问Prismaprisma.oauthClient、prisma.apiKey等重写为 Kysely真正的大头Rediscodes、device、noncecivitai/redis打包 key原样复用——同包、同 keyid_token签名civitai/authsigner可选主应用默认关闭复用 hub signer——此处已配置登录用户consent 门槛getServerAuthSessionNextAuthevent.locals.user已在hooks.server.ts填充Consent / device UIReact Mantine用 Svelte 重写客户端 logo 用civitai/brand客户端/consent 管理tRPC routersPhase 1 留在主应用见 §6 决策4. 迁移清单源 → 目标协议端点→apps/auth/src/routes/...下以server.ts落地主应用Hub 路由api/auth/oauth/authorize.tsGET 表单数据 POST/api/auth/oauth/authorize/server.tsapi/auth/oauth/token.ts/api/auth/oauth/token/server.tsapi/auth/oauth/userinfo.ts/api/auth/oauth/userinfo/server.tsapi/auth/oauth/revoke.ts/api/auth/oauth/revoke/server.tsapi/auth/oauth/device.ts/api/auth/oauth/device/server.tsapi/auth/oauth/device-info.ts/api/auth/oauth/device-info/server.tsapi/auth/oauth/device-approve.ts/api/auth/oauth/device-approve/server.tsapi/auth/oauth/device-token.ts/api/auth/oauth/device-token/server.tsapi/.well-known/openid-configuration.ts/.well-known/openid-configuration/server.ts用户页面→ Svelte主应用Hub 路由src/pages/login/oauth/authorize.tsx/login/oauth/authorize/page.sveltepage.server.tssrc/pages/login/oauth/device.tsx/login/oauth/device/page.sveltepage.server.ts服务端库→apps/auth/src/lib/server/oauth/server.ts复用、model.ts→Kysely、token-helpers.ts→Kysely、constants.ts、oidc-nonce.ts、audit-log.ts、rate-limit.ts、errors.ts。从当前仓库结构看apps/auth侧已经落位协议端点目录 apps/auth/src/routes/api/auth/oauth 下不仅有清单中的authorize/、token/、userinfo/、revoke/、device/、device-info/、device-token/还有清单之外的device-deny/、introspect/含 introspect/server.ts 及其测试、session/、legacy-exchange/可见迁移后的端点面比规划时更完整。库目录 apps/auth/src/lib/server/oauth 同样在规划的 8 个文件基础上扩充了access.ts、block-guard.ts、device-codes.ts、first-party.ts、http.ts、redirect-uri.ts、redis-atomic.ts、scope.ts等模块并配有 model.test.ts、token-helpers.test.ts、audit-log.test.ts 等并行测试文件——这正是文档风险一节要求的针对 Prisma 版本做 parity 测试的落点。5. 分阶段路线图Phase 0–7Phase 0 — 去风险spike约半天证明主应用与 hub 之间generateSecretHash的密钥一致性见 §2 警示。操作通过主应用现有流程铸造一个令牌再用 hub 的 Kysely client 对着ApiKey做一次查找校验。整个项目以此为 gate。确认node-oauth/oauth2-server能在 SvelteKit 的 Node adapter 下运行它是纯 Node预期没问题。Phase 1 — Schema 类型消费共享包文档注明此阶段与另一会话并行Kysely DB 类型正被移入civitai/db-schema包从prisma/schema.prisma生成。本阶段消费该成果而非手写类型生成类型落地后apps/auth从共享包导入OauthClient、OauthConsent、ApiKey表类型及ApiKeyType枚举删掉手写的apps/auth/src/lib/server/db/schema.ts它此前只声明了User/Account/VerificationToken。hub 的 Kysely client 重新按生成的DB接口定型。依赖协调Phase 2 的model.ts/token-helpers.tsKysely 重写依赖这三张表类型需确保 OAuth 表被纳入生成输出若另一个会话只切了现有三张表初始切片里可能没有。若生成晚于需求退路是为 OAuth 表写临时本地类型 stub、切换时删除——但优先等共享类型避免漂移。Phase 2 — 移植 OAuth 核心库直接拷贝constants.ts、errors.ts、audit-log.tsconsole 日志无改动、rate-limit.tsRedis——复用civitai/redis、oidc-nonce.tsRedis——复用。重写model.tstoken-helpers.ts的 Prisma 调用为 Kysely。这是大头且必须保持行为一致Redis 中 SHA256 哈希的 code、crypto.timingSafeEqual的密钥比较、scope 位掩码 ↔ 字符串数组的转换、强制UserRead基线、refresh→access 的级联吊销、1 小时 access / 30 天 refresh 的 TTL。server.tsnode-oauth/oauth2-server工厂原样移植。Phase 3 — 协议端点逐个实现server.ts从 SvelteKit 的requestmethod、headers、已解析 body、query构造库的Request/Response。保留全部安全属性PKCE S256 强制、state 强制、public client 按来源 CORS / confidential 通配、限流 keyauthorize 按用户、token/revoke 按 IP、审计事件。/authorize读event.locals.user做会话门槛未登录 → 重定向到/login?callbackUrlselfhub 对其他路由已有此行为。id_tokenauthorization_codegrant 且授予UserRead时用 hub 现有 signermaybeCreateSessionSigner铸造nonce 从 OIDC 上下文的 Redis key 取。hub总是配置了密钥主应用里是可选的所以 OIDC 在此默认开启——需确认这符合预期。openid-configuration由 hub 提供权威副本使用 hub URL jwks_uri指向 hub JWKS。Phase 4 — Consent device 页面Svelteauthorize.tsx→ Svelte 重写客户端名称/logo/描述、scope 列表tokenScopeLabels、记住我的选择、Authorize/Deny 按钮。用 SvelteKit form action 替换document.createElement(form)POST。Buzz 消费限额 UI当请求AIServicesWrite时显示会牵入buzzLimitSchema、simpleBuzzLimitToBudgets以及 orchestrator 调用bustBuzzLimitCache、deleteAuthSubject。这些是 HTTP/orchestrator 调用、可移植但扩大了面。选项Phase 4 先不带 buzz-limit 控件上线authorize 仍可用限额走默认值后续快速跟进。见 §6 决策 1。device.tsx→ Svelte 重写输入 code → 复核 → 批准调用 hub 的 device 端点。Phase 5 — 旧路由的重定向/代理兼容层GET 与 POST 行为不同——不能一刀切重定向。用户页面/login/oauth/authorize、/login/oauth/device308/307重定向到auth.civitai.com/...保留 query string。浏览器可正常跟随。机器端点token、userinfo、revoke、device*不要 302。第三方客户端硬编码了civitai.com/api/auth/oauth/*且不可靠地会跨源带 body 跟随重定向 POST。改为每个旧路由做薄的服务端代理fetch透传到 hub保留 method/headers/body 并中继响应 CORS。这些代理保留到遥测显示没有客户端再访问为止。Discovery主应用上的/.well-known/openid-configuration重定向GET到 hub或者继续服务但改为 hub 端点 URL让新客户端自行路由到auth.civitai.com。Discovery 是间接层——客户端一旦从它读到 hub URL就不再触碰主应用。该策略在当前仓库中的落地主应用侧的 OAuth API 已收敛为单一 catch-all 路由 src/pages/api/auth/oauth/[...path].ts文件头注释完整解释了为何代理而非重定向并实现了 Phase 5 的全部要点// Only browser-navigation endpoints redirect; every other oauth path is an API call and is proxied. const REDIRECT_ENDPOINTS new Set([authorize]); // ... // Browser flow → redirect (308 preserves method body; the browser follows transparently). if (REDIRECT_ENDPOINTS.has(endpoint)) { res.redirect(308, target); return; } // Server-to-server → transparent reverse proxy.实现要点Hub 地址来自AUTH_JWT_ISSUER环境变量去尾部斜杠未配置时返回 500server_errorbodyParser: falsereadRawBody以 Buffer 逐字节转发请求体避免 Next 先解析/消费 bodyhop-by-hop 头剥离RFC 7230 §6.1请求侧剥离host/connection/content-length/transfer-encoding等content-length/encoding由 fetch请求侧与 Next响应侧重算响应侧额外剥离content-encoding因为 fetch 的arrayBuffer已解码 body原编码头会过期Authorization/Cookie完整保留——注释说明这是代理优于重定向的全部理由跨源 308 时按 Fetch 规范敏感头会被剥离会破坏client_secret_basictoken/revoke与Beareruserinfo鉴权导致invalid_client/401Set-Cookie按数组中继多值头不能逗号拼接cf-connecting-ip/x-forwarded-for随头透传保证 hub 限流器仍能看到真实客户端 IP15 秒超时AbortController超时返回 504、其他失败返回 502均带error: server_error的 OAuth 风格错误体redirect: manual防止代理自身跟随 hub 的重定向。这比规划文档更进一步文件注释明确记录了两类静默故障场景部分 OAuth HTTP client 在 POST 上跟随后丢弃 body跨源重定向剥离Authorization头说明代理方案是在观察到客户端 access token 过期被迫走 refresh 路径的现场问题后确认的。Phase 6 — 管理面oauth-client/oauth-consentrouters这两个 tRPC router 驱动注册/管理我的 OAuth 应用开发者与已连接应用用户UI。它们是管理面而非协议读写同一个共享 DB。Phase 1 立场留在主应用的 tRPC 中。协议迁移并不要求它们。协议唯一需要的写操作——记住时 upsertOauthConsent——直接在 hub 的/authorize处理器里用 Kysely 实现与 router 解耦。若/当管理 UI 本身搬迁时再移植到 hub作为独立工作项跟踪。Phase 7 — 切换更新各 provider/app 控制台 OAuth 客户端注册表让新的 authorize/token URL 指向auth.civitai.com。弃用窗口期间保留主应用代理观察origin.rejected/ 代理命中的审计日志。代理流量趋近于零后从主应用删除src/pages/login/oauth/*、src/pages/api/auth/oauth/*、src/server/oauth/*。从当前仓库状态看切换已经走到较后阶段src/pages/login/oauth目录已不存在src/pages/api/auth/oauth只剩代理文件协议端点已全部在 hub 侧运行见 §4 的目录证据。6. 需要团队拍板的决策原文档标记了五项待决策ai:*标注处需dev输入Buzz 消费限额consent 时——Phase 4 移植前期增加 orchestrator 耦合还是先不带上线再快速跟进推荐快速跟进让首次切换尽量小。管理 routers——确认暂留主应用推荐还是同批移植到 hub。OIDC 默认开启——hub 总是有签名密钥所以id_token签发默认开启主应用用可选密钥 gate。确认这是否意图。Schema 来源——已解决另一会话正在把生成的 Kysely 类型移入civitai/db-schemahub 消费之见 Phase 1。唯一开放子点确认 OAuth 表OauthClient、OauthConsent、ApiKey在生成切片中而不只是已有三张表。代理寿命——机器端点代理保留多久、何时强制客户端切到auth.civitai.com取决于有多少第三方客户端硬编码了旧 URL。7. 风险清单令牌哈希一致性Phase 0——成败假设已 gate。落地手段是共享 secret-hash.ts 的单一实现SHA-512 NEXTAUTH_SECRETsalt、缺失即抛错并有对应测试 secret-hash.test.ts。切换时的跨源 POST——用机器端点走代理而非重定向缓解实现见 [...path].ts。model.ts的 Kysely 重写——机械但安全敏感timing-safe 比较、scope 位掩码、级联吊销。需要仔细 review 针对 Prisma 版本的 parity 测试hub 侧 model.test.ts 与 token-helpers.test.ts 即为该要求的落点。Scope/TokenScope常量 tokenScopeLabels必须在 hub 与主应用之间共享而非分叉——抽到共享包或civitai/auth不要复制。hub 侧的 scope.ts 与 scope.test.ts 对应该关注点。8. 小结这套迁移的可复用模式从这份规划及其在仓库中的最终落地可以提炼出协议类服务迁移的三个可复用模式把数据库当作信任集成点不新建令牌验证信任链两个应用共享 Postgres 与同一个哈希函数generateSecretHash放进共享包civitai/auth任何一方签发的令牌在另一方零改动可用。风险点密钥一致性用 fail fast 共享实现而非部署约定来消除。GET 重定向、POST 代理浏览器导航型端点authorize用 308 保留 method/body机器端点token/revoke/device*绝不能靠重定向——跨源重定向会被客户端丢弃 body、被 Fetch 规范剥离Authorization头。薄代理 剥离 hop-by-hop 头 15s 超时 502/504 错误语义是零客户端改动兼容层的完整配方。Discovery 文档是间接层把jwks_uri与端点 URL 指向 hub让新注册客户端自动路由到新位置旧客户端由代理托底遥测审计日志中的代理命中决定何时拆除旧路由。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表