存量账号迁移与无感切换
预绑定原账号并保持角色、业务外键和历史数据不变
存量账号迁移与无感切换
本规范只适用于 legacy-migration:已经在线运行、拥有本地用户和历史业务数据的 App。目标是替换身份提供方,同时保持原用户主键、角色、客户归属、项目负责人、创建人、审批人和审计历史不变。没有需要保留的生产账号和历史身份数据时,请使用新应用接入,不要伪造迁移计划。
1. 最终用户体验
完成预绑定后,用户在切换日只经历标准 SSO:
访问原业务 App URL
-> 跳转 IWish Auth
-> 已有 Auth 会话时直接返回;否则完成飞书或外部邮箱登录
-> App 从 Session 0.3 取得 session.user.id
-> 按 iwish_auth_user_id 命中原本地成员
-> 继续使用原角色、原业务数据和原操作记录
用户不需要重新创建业务账号,不需要重新分配角色,不需要迁移历史数据,也不需要把旧密码交给 Auth。首次切换可能需要重新登录一次;已经登录 Auth Portal 或其他 App 的用户通常只经历自动跳转。
2. 不变量
迁移必须保持:
- App 原本地用户主键不变。
- 所有业务外键继续引用原本地用户主键。
- App 本地角色、permission、字段和记录范围不变。
- 历史创建人、负责人、审批人和审计记录不变。
- Auth 只成为身份和 SSO 来源,不接管业务授权。
- 每个 App 的
iwish_auth_user_id唯一关联一个本地成员。 - production 切换前完成预绑定,不在运行期按邮箱猜测身份。
禁止:
- 用 Auth User ID 替换全部业务表的本地用户外键。
- 导入旧密码或密码 hash 到 IWish Auth。
- 根据姓名、手机号、未验证邮箱或模糊文本自动合并账号。
- 登录失败时自动创建一个没有历史数据的新本地账号。
- 把逐用户映射、邮箱、姓名、Cookie、Session 或 Secret 提交到 Git。
- 通过长期双登录、Auth Proxy 或可伪造 Header 兜底。
3. App 本地数据模型
推荐保留原成员表并新增稳定关联:
alter table public.app_members
add column if not exists iwish_auth_user_id uuid;
create unique index if not exists app_members_iwish_auth_user_id_unique
on public.app_members (iwish_auth_user_id)
where iwish_auth_user_id is not null;
不要为 iwish_auth_user_id 建立跨数据库外键。Auth 数据库与业务 App 数据库是独立系统,关联完整性由预绑定、Session 校验和本地唯一索引保证。
迁移期间建议建立 App 私有 ledger:
create table private.auth_identity_migration_ledger (
legacy_user_id text primary key,
iwish_auth_user_id uuid unique,
state text not null check (
state in ('discovered', 'prelinked', 'conflict', 'excluded', 'failed')
),
match_method text check (
match_method in (
'feishu_employee_id',
'verified_work_email',
'verified_external_email',
'manual'
)
),
source_fingerprint text not null,
evidence_hash text,
reviewed_by text,
reviewed_at timestamptz,
created_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
Ledger 必须位于不可通过浏览器 Data API 访问的私有 schema,或保存在加密离线工件中。公开迁移计划只保存计数和证据路径,不保存逐用户 PII。
4. 身份匹配优先级
| 优先级 | 方法 | 自动预绑定条件 | 冲突处理 |
|---|---|---|---|
| 1 | feishu_employee_id | 本地员工号已验证,Auth 中唯一,组织一致 | 人工核对飞书人员档案 |
| 2 | verified_work_email | 本地企业邮箱已验证,Auth 飞书员工邮箱唯一,组织一致 | 禁止自动选择 |
| 3 | verified_external_email | 外部账号已完成 Auth 邀请和邮箱验证,本地邮箱唯一 | 重新邀请或人工确认 |
| 4 | manual | 管理员核对业务归属和 Auth 身份 | 双人复核高权限账号 |
姓名、手机号、显示名和未验证邮箱不能成为自动匹配依据。一个本地用户匹配多个 Auth 用户、多个本地用户匹配一个 Auth 用户、共享账号、缺失身份或组织不一致都必须进入 conflict。
5. 七个迁移闸门
Gate 1:Inventory
- 记录本地用户总数、启用数、停用数、外部用户数和共享账号数。
- 盘点旧登录、密码重置、Session、API Token 和身份 Header。
- 盘点所有引用本地用户主键的业务表和外键。
- 盘点本地角色、permission、字段权限、数据范围和管理员入口。
Gate 2:Identity Mapping
- 从受控 Auth 管理流程取得组织用户目录。
- 生成只读匹配预览,不直接修改 production。
- 将每个存量用户分类为
prelinked、conflict、excluded或pending。 - 对管理员、财务、导出和删除权限账号进行人工复核。
- production
cutover_ready前conflict=0且pending=0。
Gate 3:Authorization Preservation
- 保留本地用户主键和业务外键。
- 对比迁移前后每个角色的权限集合。
- 对比项目负责人、客户归属、创建人、审批人和审计历史计数。
- 本地角色变化仍只在业务 App 生效。
Gate 4:Staging SSO
- 使用独立 staging App、数据库、OAuth Client、callback 和 Secret。
- 使用脱敏快照或覆盖边界情况的合成数据。
- 验证飞书、外部邮箱、无映射、停用、离职、跨 App 和 App access。
- 验证 viewer/member/manager/admin 以及记录和字段范围。
- staging 登录必须使用在线 Auth 公共文档和 SDK,不读取 Auth 源码。
Gate 5:Rollback
- 保存上一稳定部署、数据库备份和映射快照。
- 演练回滚部署与恢复映射数据。
- 回滚不能恢复长期双登录或绕过 Auth 的 Header。
- 如果 Auth 身份不可用,App 必须 fail closed。
Gate 6:Cutover
- 短时冻结本地成员和角色变更。
- 重新生成 inventory 与 mapping diff。
- 原子写入预绑定映射并校验唯一索引。
- 部署新登录版本。
- 关闭旧登录、旧密码重置和旧 Session 接受路径。
- 撤销旧人员 Session 和旧机器凭证。
- 立即验证管理员、普通成员、外部用户和关键 API。
Gate 7:Observation
- 观察至少 24 小时,推荐 168 小时。
- 监控 SSO callback、401、403、无映射、冲突和本地授权拒绝率。
- 将用户问题区分为身份失败、App access 失败、本地成员失败和本地权限失败。
- 观察期通过后才删除旧密码 hash、旧 Session 表和旧登录代码。
6. 无感切换如何实现
无感来自切换前预绑定,不来自运行期邮箱猜测:
- 切换前把
iwish_auth_user_id写入原本地成员。 - 原项目、客户和审计表继续引用本地成员 ID。
- 切换后 Session 0.3 的
user.id直接命中原成员。 - App 使用原成员继续执行原本地授权。
未预绑定用户必须显示“账号尚未完成迁移,请联系管理员”,不能创建空账号。切换后新增用户可以走 App 明确设计的新成员 onboarding,但该流程与存量迁移分开。
7. 内部员工和外部账号
内部员工:
- 使用飞书 SSO。
- Auth
employee.employmentStatus必须为active。 - 离职或停用优先阻断,即使 App 本地角色仍存在。
- 历史业务记录不删除。
外部账号:
- 先通过 Auth 邀请并完成邮箱验证。
- 再预绑定到 App 原外部成员。
- 旧 App 密码不迁移;首次使用 Auth 时由 Auth 完成激活或密码设置。
- 不要求外部客户使用飞书。
共享账号必须在迁移前拆分。机器任务必须迁移到 Service Account/Service Token,不能绑定人员 SSO。
8. 迁移计划 1.0
每个 App 必须提交一个无 PII 的 auth-migration-plan.json:
{
"$schema": "https://auth-docs-staging.iwishapp.cn/account-migration.schema.json",
"schemaVersion": "1.0",
"planVersion": "2026.07.24-1",
"environment": "staging",
"appKey": "legacy_crm",
"strategy": "direct_cutover_prelinked",
"status": "mapped",
"identityLink": {
"localUserIdColumn": "id",
"authUserIdColumn": "iwish_auth_user_id",
"preserveLocalUserId": true,
"localAuthUserIdUnique": true,
"runtimeEmailAutolink": false,
"allowedMatchMethods": ["verified_work_email", "manual"]
},
"authorization": {
"owner": "application",
"preserveLocalRoles": true,
"preserveBusinessForeignKeys": true
},
"inventory": {
"total": 12,
"prelinked": 10,
"conflicts": 0,
"excluded": 2,
"pending": 0
},
"cutover": {
"mode": "direct",
"oldLogin": "disable_at_cutover",
"oldSessions": "revoke_at_cutover",
"passwordHashes": "retain_encrypted_until_observation_end",
"rollback": "previous_deploy_and_mapping_snapshot",
"observationHours": 168
},
"dataHandling": {
"mappingLedgerLocation": "app_private_database",
"piiInGit": false,
"secretsInGit": false
},
"gates": [
{ "id": "inventory", "status": "passed", "evidence": ["evidence/inventory.json"] },
{ "id": "identity_mapping", "status": "passed", "evidence": ["evidence/mapping-summary.json"] },
{ "id": "authorization_preservation", "status": "pending", "evidence": [] },
{ "id": "staging_sso", "status": "pending", "evidence": [] },
{ "id": "rollback", "status": "pending", "evidence": [] },
{ "id": "cutover", "status": "pending", "evidence": [] },
{ "id": "observation", "status": "pending", "evidence": [] }
]
}
校验命令:
iwish-auth migration validate ./auth-migration-plan.json --environment staging
Schema 只能保证格式和跨字段闸门,不替代人工身份复核、数据库备份和真实 E2E。
9. 最低验收矩阵
- 原本地用户主键和所有关键业务外键前后完全一致。
- 已预绑定内部员工通过 SSO 返回原账号。
- 已预绑定外部用户通过邮箱登录返回原账号。
- 无映射、重复映射和冲突账号 fail closed。
- App access 撤销、Auth 停用和飞书离职立即阻断。
- 本地角色和数据范围迁移前后一致。
- 旧 Session 在切换后不可继续使用。
- 旧密码登录和密码重置入口不可访问。
- 回滚可以恢复上一部署和映射快照。
- 逐用户映射、Secret、Session 和 Cookie 不进入 Git、构建产物或日志。
10. AI Agent 执行规则
AI 必须先生成 inventory 和只读计划,再修改认证代码。AI 不得根据猜测生成真实 Auth User ID,不得把 synthetic ID 写入 production seed,不得要求用户在聊天中提供密码、Cookie、Session 或 Client Secret。
如果目标 App 存在重复邮箱、共享账号、缺少稳定身份、未知权限分支或无法解释的业务外键,AI 必须把账号标记为冲突并停止 production 切换,不能用“先上线再修复”绕过。