Civitai 认证体系:NextAuth 到集中式认证 Hub(auth.civitai.com)的迁移全景
2026/9/17 20:55:17 网站建设 项目流程

Civitai 认证体系:NextAuth 到集中式认证 Hub(auth.civitai.com)的迁移全景

【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai

本篇围绕 docs/auth/auth-index.md 这份认证文档总索引展开:它是 Civitai monorepo 中 "NextAuth → 集中式 Hub" 认证迁移的唯一入口文档。读完本文,你能掌握该迁移的现状(主应用已完成切换、薄 ES256 令牌模型、OAuth2/OIDC Provider 迁移被推迟)、Hub/Spoke 拓扑与@civitai/auth包边界的设计原理、跨域(civitai.red / localhost)登录的 auth-code 桥机制,以及多账号切换、管理员 impersonation 等核心流程的实现方式,并能按索引中的分诊结构找到部署 runbook、发布流程与待办清单等配套文档。

1. 迁移背景与当前状态(2026-06-17 快照)

索引文档开宗明义:本次迁移将apps/auth部署到auth.civitai.com,使其成为唯一的登录权威与 Session 颁发方(sole login authority / session issuer)。civitai.com(同一注册域)和 civitai.red(因法务原因属于独立注册域)都通过它完成认证;Session 由一个薄 ES256civ-tokencookie加共享 Redis 中的SessionUser构成,各 Spoke 应用通过 JWKS 本地验签。

索引文档中 "Current state (2026-06-17)" 一节给出了四条权威状态结论:

  • 主应用已切换:NextAuth 已从主应用删除([...nextauth].tsnext-auth-options.ts均已移除),应用内社交登录 UI 也一并去掉,/login现在只是到 Hub 的服务端重定向;
  • 薄 ES256 令牌是已交付(shipped)的模型,它取代了部分旧文档中的"胖 RS256 令牌"表述(这一点在索引中明确标注,避免读者被过时文档误导);
  • OAuth2/OIDC Provider 迁入 Hub 的迁移被推迟(2026-06-17 决策),当前 Provider 在主应用中处于休眠状态(signing 由"是否配置了密钥"这一条件门控);
  • 未解决的 swap-bridge / Hub 阻塞项(B1 开放重定向、B2 封禁不吊销的 no-op 桩、B4 无 Redis 时 swap fail-open、B5.env.example的 RSA vs ES256 不一致、M2 登出 device-cookie)会带入生产环境,在推迟的 OIDC 收敛完成前,swap bridge 会一直保留,这些阻塞项在 cutover review 中跟踪。

索引同时说明了一个重要的文档卫生原则:安全 review、cutover findings、生产 lockout 调查等记录不存放在本仓库——因为这是公开仓库,任何包含未修复项的 findings 清单对攻击者而言就是一份待办清单,这些记录放在私有 infra 仓库中(见CLAUDE.md的 Security 一节)。

2. Hub ↔ Spoke 架构总览

索引的 "Start here" 分诊组把 docs/auth/auth-hub-spoke-overview.md 标记为架构spec——描述认证在 monorepo 中"应该"如何工作(意图,而非切换步骤)。其核心角色划分:

  • Hubapps/auth(SvelteKit 应用,auth.civitai.com),Session 的唯一生产方与颁发方。负责登录(OAuth/邮箱)、签发 session token、持有签名私钥、发布公钥(JWKS);
  • Spokes:其余所有应用——主应用(civitai.com,Next.js)、civitai.red(同一套 Next 代码、不同注册域)、moderator 应用。Spoke 是纯消费方:验证并读取 session,但绝不签发;
  • @civitai/auth:任何应用与 Hub 之间的唯一接口,所有 Hub 交互都必须经由它完成。

拓扑演进的决策来源是 docs/auth/centralized-auth-app.md(2026-06-09,标题即 "hub issues, spokes verify" 的拓扑决策文档),该文档论证了为什么"应用与 SDK 两者都需要":应用是部署物、无法被 import,Spoke 必须在本地运行的验证/接收代码只能以包的形式存在——App = 颁发方 surface;Package = 验证/接收 SDK。注意该文档顶部标注 SUPERSEDED/HISTORICAL:其中的 swap-token 桥与USE_HUB_SESSION已随迁移移除,跨域登录现在走 OAuth authorization-code + PKCE 一方桥(详见 docs/auth/spoke-integration-guide.md)。

3. 薄 Session 令牌(civ-token)模型

索引把 docs/auth/thin-session-token-design.md 标注为"decided",并明确指出它取代了 docs/auth/auth-hub-launch-checklist.md 中 #8/#11 的胖令牌表述。该设计的关键结论:

Cookie 只携带身份{ sub: <userId>, signedAt: <epoch ms>, jti: <tokenId>, iss, exp, kid }不内嵌任何用户数据。设计文档给出的"决胜理由"是跨根域一致性:.com.red是不同注册域、各自独立建立 cookie,胖 cookie 意味着 N 份独立快照按各自节奏刷新、必然漂移;薄 cookie 没有东西可漂移——每个根域都从同一个共享源解析用户,天然一致。

富用户按需解析:Hub 是 session-user 数据的唯一生产方——用 Kysely 查询 Postgres(jsonObjectFrom/jsonArrayFromUser+profilePicture+referral+customerSubscription→product→price折叠成一次成形读取),再做 SQL 做不了的派生(tier 排序、getUserBanDetailsuserSettingsallowAds/redBrowsingLevel),从 system-permissions sysRedis 缓存读permissions(带 degraded-skip 规则),最后把富SessionUser写入共享缓存session:data2:{userId}。Hub 同时暴露GET /api/auth/identity(read-through:缓存热则直接返回,miss 则即时生产;吊销在同一调用内强制)与POST /api/auth/identity(服务鉴权,AUTH_INTERNAL_TOKEN,是唯一的缓存失效原语,refresh:true可立即重新生产并返回新用户)。

消费端零配置:其余所有应用只是消费者,唯一构造器createSessionClient({ isRevoked })就是完整的消费面:

const session = createSessionClient({ isRevoked }); const user = await session.getSessionUser(token); // 读:verify → 共享 redis → miss 则 GET {iss}/api/auth/identity await session.invalidate(userId); // 写:bust const fresh = await session.refresh(userId); // 写:bust + 重新生产

查找 URL 是自描述的——令牌iss声明即 Hub 地址(AUTH_JWT_ISSUER),大部分请求命中 Redis,只有缓存 miss 才落到 Hub API;Hub 不可达的冷 miss 返回 null。

令牌内容在包内有单一事实源。packages/civitai-auth/src/constants.ts 集中定义了认证契约常量:

export const SESSION_COOKIE_BASE = 'civ-token'; // 薄 session cookie(刻意区别于旧 next-auth 的 civitai-token,避免切换期互踩) export const DEVICE_COOKIE_BASE = 'civ-device'; // 每浏览器设备 id(账号切换 device set) export const LEGACY_SESSION_COOKIE_BASE = 'civitai-token'; // 旧 NextAuth cookie,切换期只读 export const SECURE_COOKIE_PREFIX = '__Secure-'; export const CIVITAI_OWNED_DOMAINS = ['civitai.com', 'civitai.red', 'civitaic.com'] as const;

文件头注释点出了这类常量的意义:"这里的漂移 = 静默的认证破坏"(cookie 命名碰撞曾正是这类 bug)。packages/civitai-auth/src/cookies.ts 则把 cookie 名的 secure 前缀规则收敛到一处:生产(https)名为__Secure-civ-token,开发(http)为civ-token,Hub 与所有 Spoke 由此保持一致;isSecureCookie()刻意使用||而非??读取NEXT_PUBLIC_BASE_URL || AUTH_JWT_ISSUER,防止"存在但为空串"的环境变量导致两个应用算出不同 cookie 名、读不到彼此的 session。

滚动刷新与吊销:薄令牌是固定窗口,超过 update age(约 24h 活跃)后由 Spoke 请 Hub重铸同一 session(同用户、新窗口)——只有 Hub 能铸。吊销模型分五层:封禁/禁言走解析记录内的bannedAt/muted;"登出所有设备"用sessionsValidAfter时间戳对比token.signedAt;单会话登出走 per-jti标记;全局 refresh 等价于缓存 bust;真正的"全局登出"(break-glass)则是kid 密钥轮换

4. 验证策略:共享密钥(Path A) vs JWKS 混合(Path C)

docs/auth/auth-verification-strategy.md 回答了索引中"是否认证变更会强制消费者重新部署"这一问题。两条候选路径都保持本地验证、无每请求网络跳

  • Path A——共享库 + 对称密钥:Spoke import@civitai/auth,用共享NEXTAUTH_SECRET进程内验 cookie。最简、延迟最低,但密钥分发到每个应用,爆炸半径大(任一泄露的 Spoke 都能铸造合法 session);
  • Path C——非对称 JWT + JWKS 混合:Hub 用私钥签名,Spoke 用缓存的公钥(JWKS 端点)本地验签 + Redis 吊销检查。私钥永不出 Hub,泄露的 Spoke无法铸币;密钥轮换、Provider 变更、令牌内容变更均零消费者重部署,新应用接入只需提供 JWKS URL。

选型结论是目标 C、经由 A 分阶段到达:A 是 C 工作量的严格前缀。实际交付时算法定为ES256 / EC P-256——该文档 2026-06-17 的更正说明明确指出,已交付的 Path C 用的是 EC P-256 而非 RS256/RSA,且 RSA 密钥现在会在启动时直接抛错。源码印证了这一点:packages/civitai-auth/src/sign.ts 中ALG = 'ES256'assertEcP256守卫把"密钥类型配错"(典型原因是过时的 keygen 文档)转成一条可操作报错,并给出正确的生成命令:

openssl ecparam -genkey -name prime256v1 -noout -out priv.pem openssl ec -in priv.pem -pubout -out pub.pem

选择 ES256 而非 RS256 的理由写在同一文件的注释里:同样是非对称 + 可发布 JWKS,但签名只有约 64 字节(RS256 为 256 字节),session cookie 与 OIDC id_token 都更小。同一个 Hub 密钥同时签名 session 与第三方 "Sign in with Civitai" 的id_tokenmintIdTokenaud为 client_id、回显nonce防重放,默认 1h 有效期),任何标准 OIDC 库都可经公钥 JWKS 验证。

算法切换是一个令牌格式变更,文档给出了迁移窗口保证无人被登出:

  1. 窗口期内 Spoke 同时接受旧算法与 JWKS 新签名;
  2. Hub 签发切换到新算法;
  3. 旧令牌最大 TTL(如 30 天,或强制重新认证)过后,停止接受旧算法并退役旧密钥。

密钥轮换 runbook 的要求是:同时发布新旧公钥,在超过令牌最大 TTL 后再退役旧 kid。

5. 包边界:所有 Hub 交互只走@civitai/auth

架构 spec 的 §6 把"包边界"列为关键不变量任何应用绝不手搓对 Hub 的请求。从源码结构看,packages/civitai-auth/src/的导出面与文档描述一一对应:服务端→Hub 客户端createSessionClient(token→user、invalidate/refresh)、createDeviceAccountClient(账号集 list/switch/remove,device-client.ts)、createSessionTokenClient(滚动刷新 + 吊销)、createImpersonationClient(impersonate/exit)、Hub 专用的createSessionSigner、以及createAuthVerifier;浏览器→同源代理客户端@civitai/auth/clientlistAccounts/switchAccount/removeAccount/impersonate/exitImpersonation)。应用里触碰 Hub 的 API 路由只是薄代理:转发浏览器 cookie、补上框架胶水(如设置响应 cookie),不内联 Hub URL 或请求形状;客户端组件则永远不直接 fetch 认证端点,而是走应用的AccountProvider账号上下文。

动机在文档中写得很直白:单一生产方 + 单一契约,意味着 Hub 路由或 session 形状变更只触及一个包,而不是几十个调用点。packages/civitai-auth/README.md 给出了接入方式与运行所需环境变量(均为 schema 可选,但 Spoke guard 功能上需要前两个):

// package.json "@civitai/auth": "workspace:*" // Next: transpilePackages: ['@civitai/auth'];Vite: ssr.noExternal: ['@civitai/auth']
变量用途
AUTH_JWT_ISSUERHub origin——验证 JWTiss并构造登录重定向
AUTH_JWKS_URIHub 公钥,本地 ES256 验签
AUTH_INTERNAL_TOKEN服务间密钥,用于 INTERNAL 鉴权的 Hub 读穿(/api/auth/identity

Hub 侧另需签名密钥环境变量:AUTH_JWT_PRIVATE_KEY(PKCS8)、AUTH_JWT_PUBLIC_KEY(SPKI)、AUTH_JWT_KID,均为 Hub 专用(见 packages/civitai-auth/src/env.ts 的 schema 与 packages/civitai-auth/src/sign.ts 中createSessionSigner的缺钥即抛错逻辑)。README 还列出两个典型坑:

  • Redis 可选但耦合:session client 经@civitai/redis读共享缓存,Redis 缺席时 fail-open 到 Hub identity fetch;但若设置了REDIS_URL就必须同时设置REDIS_SYS_URL,部分配置会抛错并被 fail-open 捕获——表现为"静默丢失缓存";
  • 没有 Redis 就没有吊销:不注入isRevoked时,门禁只剩签名 + 有效期检查(已登出/被封的令牌会一直解析到过期)。

Spoke 门禁的典型用法:

import { createSpokeGuard } from '@civitai/auth'; export const guard = createSpokeGuard({ require: (u) => u.isModerator === true }); // guard.check(cookieHeader, returnUrl) -> { status: 'ok' | 'login' | 'forbidden', ... }

框架适配器只需约 5 行:login→ 重定向 Hub,forbidden→ 403,ok→ 写入locals.user。参考实现是 apps/moderator。

6. 跨域登录:civitai.red 与 localhost 的 auth-code 桥

不同注册域看不到.civitai.comcookie,这是浏览器约束而非部署问题。架构 spec §7 描述了现行机制:Hub 签发短期授权码(OAuth auth-code + PKCE 的一方桥),带着它重定向到 Spoke,Spoke 在服务端到 Hub 兑换(POST /api/auth/oauth/session)得到自己的civ-token(存自己的 cookie)。localhost对生产 Hub 开发就是另一个这样的跨域 Spoke;cookie 的 secure 属性必须跟随Spoke 自身的服务协议(http localhost ⇒ 非 secure cookie),而非颁发方。

该节还解释了一个曾经的真实 bug:Hub cookie 就是 civitai.com 的 session cookie——Hub 设Domain=.civitai.com,主应用对同名 cookie 设Domain=civitai.com,RFC 6265 视其为同一 cookie 槽。于是"为了到达 civitai.red 而登录"会悄悄把 civitai.com 重指到新账号,而.red上切回旧账号走的是不触碰 Hub cookie 的 device switch,导致.com停在错误的账号上。一次性的、路径作用域的civ-pending记录就是为打破这层耦合而设:账号切换场景下 Hub 不写自己的 cookie,身份经civ-pending单次消费后送达/api/auth/oauth/authorize。包内常量印证了这个往返的两端(packages/civitai-auth/src/constants.ts):登录链接上的查询参数SYNC_PARAM = 'sync-account',以及 Spoke 侧落点SPOKE_AUTHORIZE_PATH = '/api/auth/authorize'——两端都依赖这个精确字符串,漂移会让 Hub 静默停止交接、回退写自己的.civitai.comsession,"没有报错也没有失败的测试"。

7. 多账号切换与管理员 Impersonation

架构 spec §5 定义了两条设备级能力:

  • 设备级账号切换:Hub 维护每浏览器设备集——httpOnly 的civ-devicecookie 映射到 Redis setdevice:accounts:{deviceId}userId → lastSwitchedAt30 天滚动)。切换的授权条件是:存在活跃 session,目标账号在该设备集内且新鲜(<30 天);没有客户端持有的凭据,也没有 DB 层的 User↔User 关联(因此无跨设备关联面)。浏览器另保留一份持久、无凭据的名单(localStorage:{id, username, avatar}),只用于让用户看到"这里用过哪些账号";超出 30 天窗口的账号仍会列出,但点击需要重新登录;
  • 管理员 impersonation:唯一的授权是请求者本人的 session 是 moderator(无内部 token、无额外凭据)。Hub 为目标铸一枚携带impersonatedBy: <moderatorId>声明的civ-token;"退出"读该声明并重新铸回 moderator 自己的 session。Impersonation 不触碰设备账号集,审计(ModActivity)由应用侧写入。

8. 安全不变量与文档索引导航

架构 spec 的"安全不变量"一节是整套迁移必须始终成立的硬约束:签名私钥只存在于 Hub(Spoke 仅验证);civ-token是 identity-only,token 体内不含可信任的授权/角色数据(角色来自解析出的用户);无客户端持有的长效凭据(旧的 localStorage AES token 已移除);服务间 Hub 调用使用专用的AUTH_INTERNAL_TOKEN,绝不使用用户 session token;机密永不入库,.env被 gitignore。

除上述"Start here"、"Visual reference"(两份 HTML 流程图:UC1–UC8 登录/登出/切换/impersonation 的已实现流程与 cookie 参考、swap-token 现状 vs OIDC auth-code 提案的线路图)与 SSO 简化路线(docs/auth/first-party-sso-vs-oauth-analysis.md、docs/auth/auth-login-simplification.md、docs/auth/oauth-first-party-migration-plan.md)之外,索引文档按任务把全部认证文档分成五组,构成按任务检索的分诊表:

  • Cutover 执行(NextAuth → Hub):docs/auth/main-app-auth-cutover.md(主应用切换状态与"ship 前剥离 NextAuth"决策)、docs/auth/auth-hub-main-app-changes.md(⚠️ 部分描述已废弃的胖 RS256 模型,已被薄令牌设计取代)、docs/auth/auth-hub-launch-checklist.md(⚠️ #8/#11 同被取代)、docs/auth/drop-main-app-social-login.md(移除应用内社交登录按钮的清单,已基本落地);
  • OAuth2/OIDC Provider 迁移(推迟):docs/auth/oauth-provider-to-auth-app.md(分阶段计划)、docs/auth/oauth-provider-implementation-checklist.md(含 §I:一方桥 → OIDC)、docs/auth/oauth-scoped-tokens.md 及其 checklist/review、docs/auth/oauth-resume-state.md(feature/scoped-tokens分支恢复状态)、docs/auth/oauth-developer-docs.md(对外 Civitai OAuth 开发者指南);
  • Review 与整合待办:docs/auth/auth-prelaunch-action-checklist.md 是从全部文档推导出的整合 to-do(阻塞项、运维缺口、包边界、文档卫生、推迟工作),带来源位置(2026-06-17);
  • 部署入口:索引顶部特别提示——正在部署的读者直接看docs/auth/auth-hub-deployment-plan.md,它是当前唯一整合的部署 runbook(交付内容、DB 前置、env、有序切换、风险、验证、回滚),各分阶段清单都汇入它;
  • 运维/杂项:docs/auth/releasing.md(如何切一个 Hub 发布:pnpm run release:auth[:minor|:major]auth-app-vX.Y.Ztag → 集群内 Tekton 构建 → ghcr → Flux)、docs/auth/session-refresh-debug-instrumentation.md(session-refresh 调查中临时诊断日志的追踪,便于干净回退)、docs/auth/post-deploy-domain-env-consolidation.md(后续计划:把重叠的 domain/origin 环境变量收敛到 color-map 唯一事实源、退役NEXT_PUBLIC_BASE_URL——这也与本文第 3 节isSecureCookie()读取的正是这个变量相呼应)。

9. 小结

这份索引文档的价值不在于自身承载多少细节,而在于它把一次正在进行、且有"新旧表述并存"风险的架构迁移收敛成了单一入口:现状以 2026-06-17 快照为准(薄 ES256 令牌、主应用已切换、OIDC Provider 迁移推迟、五类已知阻塞项),架构意图以 docs/auth/auth-hub-spoke-overview.md 为 spec,可执行的验证/签名/cookie 契约以 packages/civitai-auth 的源码为准,部署动作以 docs/auth/auth-hub-deployment-plan.md 为 runbook。对维护者而言,最实用的两条纪律来自索引与 spec 本身:新增认证文档必须归入对应分组并附一行用途说明;引用旧文档前先核对其是否被薄令牌设计取代。

【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询