ARTICLE DETAIL

资讯详情

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

Civitai 账号切换与模拟登录(Account Switching Impersonation)机制解析

Civitai 账号切换与模拟登录(Account Switching  Impersonation)机制解析 Civitai 账号切换与模拟登录Account Switching Impersonation机制解析【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai导读本文以仓库文档 docs/features/account-switching.md 为主体结合civitai/auth包与主应用、认证中心hub的源码完整解析 Civitai 的多账号切换与管理员模拟登录impersonation两套机制从认证中心的令牌铸造、权限门禁、审计落库到主应用的同源代理与前端账号菜单再到浏览器本地存储的演进与错误处理。读完你将掌握这套加密令牌 中心化会话方案的完整调用链以及如何在自己的 Civitai 应用中接入账号切换与模拟登录能力。一、概述两个相互独立的功能账号切换与模拟登录系统允许用户管理多个账号并允许版主moderator临时以其他用户身份操作用于客服与内容审核场景。整套系统通过加密令牌保证认证安全并依托认证中心hub维护会话完整性。原文档将其划分为两个边界清晰的功能特性账号切换Account Switching模拟登录Impersonation适用对象所有用户仅版主moderator核心能力关联多个认证提供方Google、Discord 等免重新认证快速切换临时以他人身份行动保护手段加密令牌 会话管理功能开关impersonation 权限校验审计要求无记录ModActivity审计日志视觉标识当前账号绿色对勾红色发光水晶球图标退出方式菜单切换 / 登出一键切回原账号需要特别指出仓库源码显示该机制经历过一次重要演进cutover约 2026-06-22。原文档描述的浏览器本地持有各账号加密令牌civitai-accounts属于早期方案当前仓库已改为**中心化 Hub 设备集device set**方案。后文会同时保留原文档的设计骨架并逐一给出当前源码的准确实现。二、认证流程加密令牌的设计骨架2.1 令牌结构与加密原文档给出的令牌结构基于 AES-256-CBC 加密其数据结构EncryptedDataSchema如下{ iv: string, // Initialization vector (base64) data: string, // Encrypted user ID (base64) signedAt: string // ISO timestamp }加密密钥使用NEXTAUTH_SECRET确保令牌只能由同一台服务器解密1. 生成随机 16 字节 IV 2. 以服务端密钥创建 AES-256-CBC 密码器 3. 加密用户 ID 4. 返回携带 IV 与时间戳的 base64 令牌2.2 从 NextAuth 到 Hub 原生会话原文档描述src/pages/api/auth/civ-token.ts在用户登录时生成可切换令牌并配套 NextAuth 的自定义凭据提供方CredentialsProvider({ id: account-switch, name: Account Switch, credentials: { iv, data, signedAt }, authorize: async (credentials) { // Decrypt token to get user ID // Fetch and return user session } })需要澄清的是该路径在当前仓库中已被淘汰。在 src/components/CivitaiWrapped/AccountProvider.tsx 中civitai-accounts存储被视为已退役的按账号持令牌存储RETIRED pre-cutover token-per-account store并在每次挂载时通过purgeLegacyAccountStore()主动清除——因为它持有每个账号的令牌却没有任何代码还能兑现这些令牌任由其常驻 localStorage 是安全隐患。当前切换会话由认证中心hub位于 apps/auth统一铸造与校验。三、数据库 SchemaModActivity 审计表模拟登录涉及审计原文档给出ModActivity表结构ModActivity { userId Int -- Moderator performing action entityType String -- Type of entity (impersonate) activity String -- Action taken (on or off) entityId Int -- Target user ID createdAt DateTime -- When action occurred }当前实现由 hub 写入这张共享表见 apps/auth/src/lib/server/auth/mod-activity.tsexport async function trackImpersonation( moderatorId: number, targetUserId: number, activity: on | off ): Promisevoid { await sql INSERT INTO ModActivity (userId, entityType, activity, entityId) VALUES (${moderatorId}, impersonate, ${activity}, ${targetUserId}) ON CONFLICT DO NOTHING .execute(db); }两个实现细节值得注意entityType固定为impersonateactivity取on开始模拟或off退出模拟ON CONFLICT DO NOTHING刻意不指定冲突目标这样无论ModActivity是否还持有(activity, entityType, entityId)唯一索引都能正确执行——一旦挂起中的删除该唯一索引、改为纯追加表迁移落地指定目标会以 42P10 报错不写子句则在此之前会以 23505 报错。这是针对数据库演进特意设计的健壮性写法。四、Hub 端核心逻辑权限门禁与令牌铸造4.1 开始模拟登录POST /api/auth/impersonate完整流程在 apps/auth/src/routes/api/auth/impersonate/server.ts认证中心拥有全部逻辑export const POST: RequestHandler async ({ request, cookies, locals }) { const mod locals.user; if (!mod) error(401, unauthorized); const permitted mod.isModerator (dev || (mod.permissions ?? []).includes(impersonation)); if (!permitted) error(403, not permitted to impersonate); const userId await readUserId(request); if (userId mod.id) error(400, cannot impersonate self); const target await getOrProduceSessionUser(userId); if (!target) error(404, no such user); const token await mintUserSession(target, { impersonatedBy: mod.id }); // Audit BEFORE returning — if it throws, no token ships, so an un-audited impersonation can never happen. await trackImpersonation(mod.id, userId, on); setSessionCookie(cookies, token); return json({ token, userId }); };关键点权限门禁开发环境dev下任意版主可模拟生产环境必须同时满足isModerator且持有授予的impersonation权限来自会话用户的permissions由SYSTEM.PERMISSIONS计算得出。原文档所述feature-flag 保护impersonation在当前实现中对应这层权限判定。防自模拟userId mod.id返回 400。令牌铸造mintUserSession(target, { impersonatedBy: mod.id })为目标用户铸造会话并在令牌中盖上impersonatedBy声明——这是后续识别当前会话正在模拟他人的唯一权威来源。先审计后放行trackImpersonation在返回令牌之前执行若审计写入失败则令牌不发出保证未审计的模拟不可能发生。4.2 退出模拟POST /api/auth/impersonate/exit退出端点见 apps/auth/src/routes/api/auth/impersonate/exit/server.ts。它不需要请求体也不需要额外凭据——权威来自当前会话令牌上的impersonatedBy声明只有版主发起的模拟调用才会盖上该声明因此可信const modId locals.impersonatedBy; if (!modId) error(400, not an impersonation session); const targetId locals.user?.id; // 当前被模拟的用户 const moderator await getOrProduceSessionUser(modId); if (!moderator) error(404, moderator not found); const token await mintUserSession(moderator); // 普通会话无 impersonatedBy if (targetId) await trackImpersonation(modId, targetId, off); setSessionCookie(cookies, token); return json({ token, userId: moderator.id });退出时重新铸造版主自己的普通会话不再携带impersonatedBy并写入off审计记录若当前会话本就不是模拟会话返回 400。五、主应用侧同源代理主应用Next.js通过三个同源 API 代理转发到 hub。代理存在的根本原因是主应用还跨站部署于civitai.red浏览器无法直接访问 hub需要主应用代为转发会话 cookie 并回写 hub 返回的civ-token。5.1 设备级账号切换src/pages/api/auth/switch.tsconst deviceAccounts createDeviceAccountClient(); export default async function handler(req: NextApiRequest, res: NextApiResponse) { if (req.method ! POST) return res.status(405).json({ error: method not allowed }); const userId Number(req.body?.userId); if (!Number.isFinite(userId)) return res.status(400).json({ error: bad userId }); // 目标账号不在本设备集合 / 已过期 / 不可达时hub 返回 null → 客户端回退到重新登录 const result await deviceAccounts.switch(req.headers.cookie ?? , userId); if (!result) return res.status(403).json({ error: switch not allowed }); setSessionCookie(res, result.token, { deviceCookie: req.cookies[deviceCookieName()], host: req.headers.host, }); return res.status(200).json({ ok: true, userId }); }浏览器命中此端点时.civitai.com域的 cookieciv-tokenciv-device随请求携带hub 校验存在活跃会话且目标账号在本设备的账号集合内且未过期后返回新铸造的civ-token主应用将其设为会话 cookie并同步滚动设备 cookiehub 的 Set-Cookie 无法跨域回写。5.2 设备账号集合src/pages/api/auth/accounts.tsGET返回该浏览器设备集合的展示用账号列表未登录时为空数组DELETE ?userIdN从设备集合移除指定账号失败返回 502。它是账号切换器显示列表的数据来源。5.3 模拟登录代理src/pages/api/auth/impersonate.ts一个无脑透传DUMB pass-through到 hub 的代理所有逻辑权限门、令牌铸造并盖impersonatedBy、ModActivity 审计都在 hub 侧。代理只负责POST { userId }开始模拟该用户成功后用 hub 返回的令牌设置会话 cookieDELETE退出模拟hub 从当前令牌读取impersonatedBy回写版主自己的会话。值得借鉴的细节是错误透传代理转发 hub 的真实状态码与原因如 400 not an impersonation session、404、500而不是把一切失败抹平成一条通用消息status: 0表示 hub 不可达代理映射为 502。六、前端AccountProvider 与相关组件6.1 核心上下文src/components/CivitaiWrapped/AccountProvider.tsxAccountProvider是账号能力的集中提供者通过useAccountContext()暴露以下方法见 AccountProvider.tsx方法行为swapAccount(userId, callbackUrl?)按 userId 切换若目标在设备集合内且未过期则无缝切换否则跳转 hub 重新认证logout()仅登出当前账号绝不自动切入其他账号保留其余账号在列表logoutAll()清空该浏览器的整个设备账号集合并登出removeAccount(id)从 roster、遗留存储与 hub 设备集合中移除指定账号impersonate(userId)开始模拟登录成功后原地window.location.reload()exitImpersonation()退出模拟成功后原地重新加载为版主身份实现要点双存储设计civitai-account-rosterlocalStorage是持久的、无凭据的展示名单{ id, username, avatarUrl }即使会话过期用户仍能看到自己添加过哪些账号hub 设备集合Redis30 天滚动窗口决定哪些账号可以免重新登录无缝切换。超出窗口的账号仍保留在 roster 中但点击时会跳转 hub 重新认证界面上以needsLogin标记见 AccountProvider.tsx。isImpersonating判定直接来自会话的userData?.impersonatedBy不再依赖任何 localStorage 的原账号记录ogAccount概念已随 cutover 退役。模拟期间不进入 roster被模拟的目标用户不是本设备真实关联的账号绝不能混入账号切换器见 AccountProvider.tsx 的注释。跨标签页同步监听visibilitychange当页面重新可见且当前 userId 与上次会话不同时自动router.reload()保证标签页之间的账号状态一致。6.2 模拟指示按钮src/components/Moderation/ImpersonateButton.tsx显示条件仅当会话携带impersonatedBy且存在当前用户时才渲染即只在模拟进行中显示视觉红色发光的IconCrystalBall水晶球图标boxShadow: 0 0 16px 2px redTooltip展示You are acting as {username} ({id})与Click to return to your account动作点击调用exitImpersonation()期间显示 Switching back... 加载通知失败则以红色IconX通知 Failed to switch back。6.3 发起模拟入口src/components/Profile/UserContextMenu.tsx用户资料下拉菜单中版主可见 Impersonate User 菜单项UserContextMenu.tsxconst handleImpersonate async () { if (!user || !currentUser || !features.impersonation || user.id currentUser?.id) return; // ... try { await impersonate(user.id); // 成功后以被模拟用户身份重新加载 } catch (e) { updateNotification({ ... title: Failed to switch, message: (e as Error).message }); } };可见性与文档一致仅对版主isMod展示且要求features.impersonation开关开启、目标非本人。切换期间以 Switching accounts... 加载通知反馈失败则展示真实错误消息。6.4 主用户菜单src/components/AppLayout/AppHeader/UserMenu.tsx主应用头部用户菜单集成账号切换器列出所有关联账号、当前账号以绿色对勾标识、支持添加账号跳转 hub 认证、支持单账号登出或全部登出。七、数据流总览7.1 账号切换流程当前实现用户通过认证提供方登录hub 铸造civ-token并加入该浏览器的设备集合Redis30 天滚动前端将账号信息无凭据持久化到 localStorage 的civitai-account-roster用户在菜单选择其他账号前端调用swapAccount(userId)→ 命中同源代理POST /api/auth/switchhub 校验设备集合后铸造新civ-token代理将其设为会话 cookie 并滚动设备 cookie页面跳转/刷新新用户上下文生效。原文档描述的令牌存入 localStorage、前端调用signIn(account-switch, token)对应已被淘汰的 NextAuth 阶段当前实现中切换依据是 hub 设备集合不依赖客户端持有的令牌。7.2 模拟登录流程版主在用户资料菜单点击 Impersonate User前端impersonate(userId)→ 同源代理POST /api/auth/impersonatehub 校验版主身份与impersonation权限、确认目标存在、拒绝自模拟hub 铸造目标用户的civ-token盖impersonatedBy声明并写入ModActivity(on)审计代理回写会话 cookie页面以被模拟用户身份重新加载头部出现红色发光水晶球图标提示正在以 XX 身份操作版主点击图标 →POST /api/auth/impersonate/exit→ hub 读取impersonatedBy、重铸版主会话、写ModActivity(off)审计页面重新加载恢复版主身份。八、localStorage 结构从旧方案到新方案原文档记录的旧结构pre-cutover// civitai-accounts已退役会被自动清除 { 123: { token: { iv, data, signedAt }, active: true, email: userexample.com, username: username, avatarUrl: https://... } } // civitai-og-account模拟期间已随 cutover 移除 { id: 456, username: moderator_name }当前实现的新结构AccountProvider.tsx// civitai-account-roster —— 持久、无凭据的展示名单 { 123: { id: 123, username: username, avatarUrl: https://... } }差异要点新 roster不持有任何令牌仅作展示即使会话过期仍可列出账号哪些账号可无缝切换由 hub 设备集合Redis 30 天滚动窗口决定前端只读模拟登录不再需要civitai-og-account原账号身份由会话令牌中的impersonatedBy声明天然携带旧的civitai-accounts会在每次挂载时被purgeLegacyAccountStore()清除见 AccountProvider.tsx这是对遗留凭据的安全清理。九、错误处理原文档列出的常见错误在当前实现中均可对应到实际返回错误状态码触发条件代码位置Unauthorized401未登录调用模拟接口impersonate/server.tsNot Permitted403非版主或生产环境缺impersonation权限同上第 22-23 行Cannot Impersonate Self400目标用户是本人同上第 26 行No Such User404目标用户不存在同上第 28-29 行Not an Impersonation Session400当前会话并无impersonatedBy却请求退出exit/server.tsHub Unreachable502代理映射status: 0hub 不可达或未配置impersonation-client.tsBad userId400切换/模拟请求体 userId 非法switch.ts此外impersonation-client.ts 会从 hub 响应体读取{ message }SvelteKit 错误体或{ error }代理/JSON 错误体把真实原因透传给调用方避免错误被吞掉。十、未来增强方向原文档列出的潜在改进方向可以作为后续实施的参考清单令牌过期机制以增强安全性当前civ-token的时效由会话生命周期与设备集合窗口共同约束模拟登录必须填写原因模拟会话设置时间上限增强审计记录结束时间等基于角色的模拟限制通知被模拟的用户只读模拟模式。从当前实现看hub 设备集合的logoutAll目前依赖前端对每个账号逐条DELETE见 AccountProvider.tsx 中的TODO(E)注释未来可新增单个 forget this device 端点使整机登出原子化——这也是仓库代码中明确标注的演进方向。结语账号切换与模拟登录是 Civitai 认证体系中最贴近业务的两项能力前者用无凭据展示名单 hub 设备集合实现了免重新登录的多账号切换后者用impersonatedBy令牌声明 ModActivity 审计支撑了可追溯的版主支持流程。整个链路从 packages/civitai-auth 的纯逻辑客户端到主应用的同源代理再到 apps/auth 的 hub 端点每层职责单一、错误透传完整是研究中心化认证 多端代理架构的优质参考实现。若需了解 hub 与 spoke 的整体心智模型可继续阅读 docs/auth/spoke-integration-guide.md 与 docs/auth/main-app-auth-cutover.md。【免费下载链接】civitaiA repository of models, textual inversions, and more项目地址: https://gitcode.com/GitHub_Trending/ci/civitai创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表