civitai 认证中心 apps/auth:中心化登录 Hub 的架构、上游 OAuth 流程、RS256 会话签发与独立发布流

发布时间:2026/9/17 5:42:09
civitai 认证中心 apps/auth:中心化登录 Hub 的架构、上游 OAuth 流程、RS256 会话签发与独立发布流 civitai 认证中心 apps/auth中心化登录 Hub 的架构、上游 OAuth 流程、RS256 会话签发与独立发布流【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai本篇基于 apps/auth/README.md 展开讲清 Civitai 仓库中civitai/auth-app这个 SvelteKit 登录/授权中心hub的职责边界与技术实现它是整个系统中唯一的令牌session JWT签发方其余所有 Civitai 应用都作为 spoke 通过 JWKS 端点本地校验它签发的 RS256 JWT。读完后你应能掌握 hub-and-spoke 认证拓扑、通用 Authorization-Code PKCE 上游登录流程、环境变量与 JWKS/跨根域换票接口的设计以及release:auth独立发版流的完整操作。1. 定位唯一签发方的登录/授权中心apps/auth包名civitai/auth-app见 apps/auth/package.json当前版本0.1.34是 docs/auth/centralized-auth-app.md 中提出的 login/authorizationhub的落地实现技术形态为 SvelteKitSvelte 5 sveltejs/adapter-node。README 明确其核心地位It is the only tokenissuer; every other Civitai app is aspokethat verifies the JWT it mints (Path C).也就是说登录界面、上游 OAuth 提供商对接、会话签发、以及 JWKS 公钥分发全部收敛到这一个可部署应用里其他应用主站、apps/moderator等不再各自持有登录逻辑只做两件事本地校验会话以及校验失败时重定向回 auth 中心携带callbackUrl。该拓扑的设计背景从点对点 civ-token 交换演进为 hub-and-spoke在 docs/auth/centralized-auth-app.md 中有完整的动机与历史校验策略的选型对称密钥 vs 非对称 JWKS 的 Path A/Path C 对比见 docs/auth/auth-verification-strategy.md。1.1 技术栈README 指出其技术栈镜像了civitai-advertising应用的选型从 apps/auth/package.json 的依赖可以逐一对应SvelteKit Svelte 5 Vite adapter-nodesveltejs/kit ^2.43.2、svelte ^5.0.0、vite ^7.0.4、sveltejs/adapter-node ^5.3.2Kysely overpgkysely ^0.28.7pg ^8.13.1直连共享的 Civitai PostgresDATABASE_URL不经过 Prisma ORM 层查询jose经civitai/authcivitai/auth是 workspace 依赖workspace:*封装 RS256 会话 JWT 的签发与 JWKS 派生其余 workspace 依赖还包括civitai/db、civitai/db-schema、civitai/redis、civitai/email、civitai/axiom日志等另有prom-client提供 Prometheus 指标、node-oauth/oauth2-server支撑它自身作为 OAuth 服务器的能力第三方 Sign in with Civitai。注意一个容易混淆的区分源码注释原文见 providers.ts这个 hub 身兼两种角色——作为客户端去接 Discord/Google 等上游提供商让用户登录本文主体以及作为服务器为第三方应用提供 OAuth/api/auth/oauth/*路由族。前者是apps/auth的核心后者是同一应用内独立的一层路由。1.2 路由面概览从 apps/auth/src/routes 目录结构可以直接读出 README What it does 一节列出的五个能力各自对应的端点能力README 原文路由Login view—/login为每个已配置的上游提供商渲染按钮routes/loginUpstream OAuth—/login/[provider]→ 提供商同意页 →/login/[provider]/callbackroutes/login/[provider]Session issuance— callback 时 find/create 用户并签发 RS256 会话 JWTcallback 路由 session-producer.tsJWKS—/api/auth/jwks与/.well-known/jwks.json服务公钥routes/api/auth/jwksCross-root swap—/api/auth/sync为不同根域 spokecivitai.red签发短命换票令牌sync-account-utility-migration.md 相关历史背景此外从路由树还能看到 hub 已长成完整认证平台的证据/api/auth/oauth/{authorize,token,device,introspect,revoke,session,userinfo}构成完整 OAuth/OIDC 服务器面/api/auth/refresh、/api/auth/switch、/api/auth/accounts支持刷新与多账号/admin/*提供 roles、membership、spoke-domains、access 的管理界面/metrics暴露 Prometheus 指标。2. 上游 OAuth一套通用流程驱动所有提供商README 将登录流程概括为/login/[provider]→ 提供商同意 →/login/[provider]/callback由lib/server/auth/providers.ts中的通用 Authorization-Code PKCE 流程实现。providers.ts 的源码印证了这一设计并给出了几个值得学习的工程细节。2.1 配置表驱动 环境变量惰性读取文件头注释明确写道Minimal, dependency-free OAuth2 Authorization-Code PKCE client. One generic flow drives every provider via a small config table. Secrets are read lazily from env, so a provider simply turns on once its CLIENT_ID/SECRET are present. 具体实现是一个PROVIDERS: RecordProviderId, ProviderDef表每个提供商声明interface ProviderDef { id: ProviderId; name: string; authorizeUrl: string; // 同意端点 tokenUrl: string; // 令牌端点 userinfoUrl: string; // 用户信息端点 scope: string; incrementalScope?: string; // 仅显式用户意图时才追加的 scope basicAuthTokenRequest?: boolean; // Reddit 要求 token 端点用 HTTP Basic userAgent?: string; emailsUrl?: string; // GitHub 的 /user/emails 补查 clientId: () string | undefined; // 注意是函数惰性读 env clientSecret: () string | undefined; mapProfile: (json: Recordstring, unknown) NormalizedProfile; }clientId/clientSecret是函数而非字符串读取发生在真正发起流程的时刻——这正是 README 环境变量表中 a provider only appears once both are set 的机制listEnabledProviders()被/login页面 server load 调用检查每个提供商的 env 是否齐备齐备才出现在登录按钮列表中。四个正式提供商及其固定端点/scope 为提供商authorizeUrlscope特殊处理Discordhttps://discord.com/oauth2/authorizeidentify emailincrementalScope: role_connections.write见 2.2Googlehttps://accounts.google.com/o/oauth2/v2/authopenid email profile—GitHubhttps://github.com/login/oauth/authorizeread:user user:emailemailsUrl: /user/emails见 2.3UA 固定为civitai-authReddithttps://www.reddit.com/api/v1/authorizeidentitybasicAuthTokenRequest: truetoken 端点 HTTP BasicUAcivitai-auth每个提供商的mapProfile把原始 JSON 归一为统一的NormalizedProfile { providerAccountId, email?, emailVerified?, name?, username?, image? }供后续 find/create 用户逻辑使用。2.2 增量 scopeDiscord Linked Roles 的教训incrementalScope字段附带了一段非常有信息量的注释Discord 的role_connections.writeLinked Roles如果写在每次登录的基础 scope 里会让整个 authorize 请求在 Discord 应用未配置 Linked Roles verification URL 时报invalid_scope而整体失败。因此改为增量授权——只有/discord/link-role流程?rolestrue对应 routes/discord/link-role才追加该 scope。注释还特意说明该 scope 是服务端定义的常量而非透传客户端 query 值客户端无法注入任意 scope——这是 OAuth scope 处理上一个典型的安全细节。2.3 GitHub 邮箱补查GitHub 在/user中返回的email在用户设为 private 时为null因此表中单独配置了emailsUrl: https://api.github.com/user/emailscallback 阶段用 access token 追加一次/user/emails调用恢复经过验证的主邮箱。这与 README Status / TODO 中 GitHub email needs the follow-up/user/emailscall for the verified primary 是同一件事源码注释表明该补查已落地fetchProfile会从emailsUrl恢复 verified primary email。2.4 stub 提供商e2e 专用、生产惰性表中还有一个[stub as ProviderId]条目注释解释了它的定位为 hub 的 e2e 环境提供一个确定性的上游 OIDC 提供商三个 URL 指向集群内的 stub-oidc-serverapps/auth/e2e从而在无真实外部提供商的情况下驱动真实的Authorization-Code PKCE callback 路径。两个防泄漏设计值得注意stub被刻意排除在共享ProviderId联合类型定义于 packages/civitai-auth之外主站账号面板因此永远不会感知它它只在AUTH_ENABLE_STUB_PROVIDER为真且STUB_CLIENT_ID/SECRET同时配置时启用否则不出现在提供商列表中/login/stub直接 404。3. 会话签发callback 到 .civitai.com CookieREADME 对签发链路的概括是on callback, find/create the user (Kysely), mint the RS256 session JWT viacivitai/auth, set it as the.civitai.comcross-subdomain cookie. 逐环拆解find/create 用户callback 路由以NormalizedProfile.providerAccountId为键在共享 Civitai 库中查找/创建用户查询走 Kyselylib/server/db。README Status / TODO 特别指出当前 DB 类型是手写的子集lib/server/db/schema.ts计划替换为从civitai/db-schema生成的prisma-kyselyDB类型与 advertising 应用对齐签发 RS256 JWTcivitai/auth基于AUTH_JWT_PRIVATE_KEYPKCS8 PEM用 RS256 签名AUTH_JWT_KID作为kid头声明AUTH_JWT_ISSUER同时充当 OIDCiss种跨子域 CookieCookie 的Domain由AUTH_COOKIE_DOMAIN指定为.civitai.com使*.civitai.com下所有子域同一可注册域内的各 spoke天然可见该会话——这是 same-root spoke 零额外工作即可免登录 的浏览器机制基础。历史上该 Cookie 曾刻意采用__Secure-前缀而非__Host-后者禁止Domain属性会破坏子域共享详见 docs/auth/centralized-auth-app.md §1a。同一/login页面还承载了邮箱 magic-link登录README 的 TODO 称其尚未实现但从源码结构看routes/login/page.server.ts 中的emailform action 与 routes/login/email/verify 已具备完整实现README 该条目应为历史遗留。该 action 的滥用防线是分层门控顺序刻意由廉价到昂贵邮箱格式正则校验按真实客户端 IP 限流——checkRateLimit(email-login, ip, 5, 600)且getClientIp从代理头解析真实 IP 而非共享 ingress 的 socket peer限流不通过直接 429Turnstile captcha——默认 invisible 组件自动注入cf-turnstile-response交互式 managed key 仅在 invisible 失败时作为兜底渲染站点 key 通过 SSR data 下发而非PUBLIC_env封禁邮箱域——精确域列表 后缀封禁列表任一命中即拒绝新注册但不会回溯锁死已有账号plus-address 反滥用——别名邮箱禁止新注册已有用户不受影响。通过全部门控后createVerificationToken(email)生成 tokenreturnUrl与跨域同步参数SYNC_PARAM一并写入验证链接由sendVerificationEmail发出routes/login/email/verify 验证时再校验它们。returnUrl的白名单校验则由 first-party.ts 的buildPostLoginOriginCheck完成防止开放重定向。4. JWKS 端点spoke 本地验签的公钥来源spoke 侧的校验路径Path C完全无状态拿 JWT 的kid去 hub 的 JWKS 取公钥本地验 RS256 签名 iss/exp无任何数据库往返。routes/api/auth/jwks/server.ts 的实现非常精简// Public keys for verifying session JWTs (first-party spokes) and OIDC id_tokens // (third parties). 404 until the hub ES256 keys are configured. const signer maybeCreateSessionSigner(); export const GET: RequestHandler async () { if (!signer) error(404, JWKS not configured); const jwks await signer.publicJwks(); return json(jwks, { headers: { cache-control: public, max-age300, stale-while-revalidate86400 }, }); };三个要点未配置即 404maybeCreateSessionSigner()在缺少密钥 env 时返回 falsy端点返回 404 而非 500——JWKS 未就绪与服务异常语义区分明确缓存头public, max-age300, stale-while-revalidate86400让 spoke/第三方 RP 的 JWKS 客户端可以放心缓存 5 分钟、并在过期后 24 小时内先用旧值降低对 hub 的读取压力一份 JWKS 两用同一端点既服务第一方 spoke 的会话 JWT 校验也服务第三方 OIDC RP 的id_token校验README 提到/.well-known/jwks.json是 OIDC 约定的同内容别名。AUTH_JWKS_URIenv 则指向本应用自己的/api/auth/jwks用于 hub 校验自己种下的 Cookie——hub 也是自己的 spoke。5. 跨根域换票/api/auth/syncCookie 无法跨越可注册域civitai.com的 Cookie 对civitai.red不可见跨根域 spoke 因此走换票路径README 概括为 /api/auth/syncmints a short-lived signed swap token for different-root spokes (civitai.red)。从 docs/auth/centralized-auth-app.md §1b 的历史实现syncAccount构造?sync-accountgreen链接 → 目标域拉取源域/api/auth/sync→ 源域用已认证 Cookie 铸造加密的 userId 传递令牌 → 目标域解密后本地铸造会话可以看出hub 化后改动的是拓扑拉取方从对端 color-domain 变为 hub-host而非换票机制本身。README 引用的设计文档同时提醒了该端点的安全边界Access-Control-Allow-Origin应收敛到已知 spoke 根域且 civ-token 需保持短 TTL 单用以约束重放该文档标记为 RESOLVED最终落地的 thin-session Cookie 为SameSiteLax跨根域换票走顶层 Lax 导航而非带凭证的nonefetch。6. 环境变量以下环境变量表完整继承自 apps/auth/README.md是所有部署配置的核心变量用途DATABASE_URLPostgres共享 Civitai 库AUTH_JWT_PRIVATE_KEY/AUTH_JWT_PUBLIC_KEY/AUTH_JWT_KIDRS256 签名密钥对PKCS8 / SPKI PEMAUTH_JWT_ISSUERissuer同时也是 OIDCiss例如https://auth.civitai.comAUTH_JWKS_URI本应用自己的/api/auth/jwks用于校验自己的 CookieAUTH_COOKIE_DOMAIN.civitai.com用于跨子域共享{DISCORD,GOOGLE,GITHUB,REDDIT}_CLIENT_ID/_SECRET每个提供商一组两者都配置后该提供商才出现结合 §2 源码可以补充三点理解AUTH_JWT_PRIVATE_KEY/AUTH_JWT_PUBLIC_KEY分别对应maybeCreateSessionSigner()的签名侧与 JWKS 发布侧密钥未配则 JWKS 404提供商 env 是惰性读取的即部署同一份镜像时可以通过 env 差异灰度启用/关闭某个提供商无需改代码AUTH_JWT_ISSUER一个值同时约束会话 JWT 与 OIDCid_token的iss声明spoke 校验时会核对它。此外 e2e 环境还涉及AUTH_ENABLE_STUB_PROVIDER与STUB_CLIENT_ID/STUB_CLIENT_SECRET见 §2.4。7. 发布流release:auth 与 tag → Tekton → FluxREADME 给出三行命令并指向 docs/auth/releasing.md 的完整流程pnpm run release:auth # patch (默认) pnpm run release:auth:minor pnpm run release:auth:majordocs/auth/releasing.md 将其展开为一条可操作的完整流水线前置条件必须在main分支且工作区干净必须有对main的 push 权限受保护分支 push allowlist。脚本行为每个脚本实际执行scripts/release-app.mjs apps/auth auth-app-v bump见 scripts/release-app.mjs依次① 校验分支与干净工作区后git pull --rebase② 只 bumpapps/auth/package.jsonnpm version --no-git-tag-version根package.json不动③ 只提交apps/auth/package.json并创建注解 tagauth-app-vX.Y.Z④git push --follow-tags。一个 monorepo 特有的坑被明确记录.git在仓库根而非包目录npm --prefix的内置 git 步骤会静默跳过commit/tag所以脚本显式自己做。推送后README 与 releasing.md 共同的描述git tag auth-app-vX.Y.Z (pushed) │ ▼ 集群内 Tekton tag-webhook 接收器监听 auth-app-v* tag │ 构建 apps/auth/Dockerfile构建上下文 仓库根 ▼ ghcr.io/civitai/civitai-auth:X.Y.Z │ ▼ Fluxdatapacket-talos 仓库ImageRepository 扫 ghcr → ImagePolicy 选最高 semver → ImageUpdateAutomation 更新主干 tag → Flux 调和 ▼ auth-hub Deployment 滚动更新关键取舍hub不使用 GitHub Actions付费 runner 且会在同一 tag 上双重建镜像构建完全在集群内 Tekton 完成也没有release分支直接从 tag 经 ghcr Flux 部署。版本号注意事项releasing.md 原文要点部署中的 hub 从0.1.0起步早于该流程手工打过一个 tag因此apps/auth/package.json同步为0.1.0后第一次脚本化发版产出0.1.1——从0.0.0起步会产出比已部署版本更低的0.0.1Flux 的最高 semver ImagePolicy 会直接忽略它因此永远不要手工推送低于已部署版本的 tag前缀约定app-vX.Y.Z此处auth-app-v用于在 monorepo 内隔离各应用 tag 命名空间避免与主应用civitai-web的v*tag 冲突未来拆出的新应用应沿用同一模式选前缀 → Tektontag-webhook挂监听 → 根package.json加release:app脚本族一次只发一个应用并发发布会在mainpush 上互相竞争。push 失败的回滚若git push被拒无main权限或发布期间main前进commit 与 tag 只存在于本地执行git tag -d auth-app-vX.Y.Zgit reset --hard origin/main后重试即可。8. 当前状态与已知待办README Status / TODO 一节完整列出引自 apps/auth/README.mdDB 类型是手写子集lib/server/db/schema.ts——计划替换为从civitai/db-schema用prisma-kysely生成的DB类型与 advertising 应用做法一致邮箱 magic-link 提供商——README 标记为未实现如 §3 所述当前源码中/login的 email action 与 verify 路由已存在该 TODO 项相对源码有滞后GitHub 邮箱——需要/user/emails的 follow-up 调用获取 verified primary源码注释表明emailsUrl补查路径已实现无 consent / 账号合并 UX新用户 provisioning 目前从简。这些待办与路由树中已存在的/api/auth/accounts、/api/auth/switch、/admin/*等能力并存说明 hub 的平台化程度已经超出 README 的最小描述——阅读 apps/auth/CLAUDE.md 与 docs/auth/auth-index.md 可以获取该应用与整个认证文档族的最新索引。9. 小结apps/auth用一份 SvelteKit 应用承担了 Civitai 全部认证面的签发职责一套配置表驱动的 Authorization-Code PKCE 客户端惰性 env 启停、增量 scope、提供商差异处理一条 Kysely find/create →civitai/authRS256 签发 →.civitai.comCookie 的会话链一个 50 行内可读完的 JWKS 端点以及一条 tag → 集群内 Tekton 构建 → Flux 按最高 semver 滚动 的独立发布流。对读者最有复用价值的三点经验是以 env 齐备性作为功能开关而非配置中心、以kid JWKS 让密钥轮换对消费者零重部署Path C、以及用app-vtag 前缀在 monorepo 中隔离多应用发布流水线。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考