> ## Documentation Index
> Fetch the complete documentation index at: https://docs.didit.me/llms.txt
> Use this file to discover all available pages before exploring further.

# 用户迁移：导入密码与 MFA

> 在 Didit 中导入用户，包括密码、已验证邮箱、社交账号关联、验证器应用和组织角色，支持 CSV 或 JSON 文件及批量导入。

> [!WARNING]
>
> 身份验证功能是新功能。请先在沙盒应用中运行每次导入，然后再导入到你的生产环境应用。

导入会把用户从其他身份平台迁移到 Didit Authentication。他们的密码、已验证的邮箱、社交登录链接、身份验证器应用和组织角色都会一并迁移。在你的应用通过 Didit 让他们登录之前，他们的使用体验不会发生任何变化。

导入使用你应用的 Authentication 管理 API，并需要具有 `write:authentication` 权限的控制台会话（若只需列出导入任务，则使用 `read:authentication`）。导入绝不使用终端用户令牌或验证 API 密钥。示例中的 `MANAGEMENT_BASE` 为：

```text theme={null}
https://apx.didit.me/auth/v2/organizations/{org_id}/applications/{application_id}/authentication/
```

## 两种导入方式

| | 文件任务 | 批量请求 |
| - | - | - |
| 端点 | `POST import-jobs/`（multipart） | `POST imports/`（JSON） |
| 大小 | 一个 CSV 或 JSON 文件，最大 25 MB，最多 100,000 人 | 每次请求 1 至 1,000 条 `rows` |
| 试运行 | 始终先在后台运行 | 默认 `dry_run: true` |
| 提交 | `POST import-jobs/{id}/start/` | 同一请求，设置 `dry_run: false` |
| 部分失败 | 每 1,000 人为一批单独提交；中断的导入会从断点继续 | 全部成功或全部失败 |

## 导入文件

### 1. 上传

```bash theme={null}
curl --fail-with-body "${MANAGEMENT_BASE}import-jobs/" \
  -H "Authorization: Bearer $CONSOLE_ACCESS_TOKEN" \
  -F file=@users.csv \
  -F passwords=import \
  -F keep_social=true \
  -F keep_verified=true \
  -F keep_mfa=true \
  -F keep_ids=true
```

| 字段 | 含义 |
| - | - |
| `file` | CSV 或 JSON，UTF-8 编码（允许带字节顺序标记），最大 25 MB。 |
| `passwords` | `import`（默认）保留密码哈希。`reset` 忽略这些哈希，因此所有人都需要设置新密码。 |
| `keep_social` | 保留社交登录链接。 |
| `keep_verified` | 保留 `email_verified`。设为 `false` 时，所有邮箱都为未验证状态。 |
| `keep_mfa` | 保留身份验证器应用的密钥。 |
| `keep_ids` | 保留 `external_id`。设为 `false` 时，`vendor_data` 将成为 Didit 身份 ID。 |
| `firebase_signer_key`、`firebase_salt_separator`、`firebase_rounds`、`firebase_mem_cost` | 你的 Firebase 项目的哈希参数。必须四个都发送，或都不发送。 |

除非你发送 `false`，每个 `keep_*` 开关默认为 `true`。调用返回 `202` 及一个任务。一个应用同时最多保留 5 个已上传的文件。

### 2. 检查试运行结果

每次上传都会先启动试运行。它会检查每个人，但不写入任何数据。轮询 `GET import-jobs/{id}/`：

| 字段 | 含义 |
| - | - |
| `kind` | `dry_run`，开始导入后变为 `import`。 |
| `status` | 试运行时为 `queued`、`running`，然后是 `passed` 或 `failed`；导入时为 `done` 或 `error`。 |
| `processed`、`total` | 进度，以人数计。 |
| `result` | 文件内容（见[结果](#结果)）。 |
| `startable` | 试运行通过且文件未过期时为 `true`。 |
| `expires_at` | 试运行后从未导入的文件，将在 24 小时后删除。 |

试运行失败时会列出错误。修正文件后重新上传。

### 3. 开始导入

`POST import-jobs/{id}/start/` 会导入试运行检查过的数据，并返回 `202`。

* 人员按每批 1,000 人导入。每批单独提交。
* 每个应用同一时间只能运行一个导入（否则返回 `import_running`）。
* 中断的导入会从最后一个已提交的批次之后继续。
* 已导入的人员会被跳过，因此可以对同一文件再次运行导入。
* 发起导入的人员会在导入完成或停止时收到一封邮件。

上传的文件以加密方式存储。导入结束、试运行失败，或试运行后从未导入且已过 24 小时，文件都会被删除。

## 分批导入

每次请求最多发送 1,000 人，并附带不超过 100 个字符的 `idempotency_key`。

```json theme={null}
{
  "rows": [
    {
      "email": "user@example.com",
      "external_id": "user-1234",
      "email_verified": true,
      "display_name": "Example User",
      "password_hash": "$2b$12$...",
      "social": [{ "provider": "google", "subject": "1234567890" }],
      "organizations": [{ "slug": "acme", "name": "Acme", "role": "admin" }]
    }
  ],
  "dry_run": true,
  "idempotency_key": "migration-batch-0001"
}
```

* 试运行返回 `200`，包含结果，且不写入任何数据。
* 设置 `dry_run: false` 时，导入成功返回 `201`。只要某个批次存在任何错误，就返回 `400`，且不导入任何人。
* 使用相同密钥重复已提交的请求，会返回其记录的结果。使用相同密钥但内容不同，则返回 `400 {"idempotency_key": "different_payload"}`。
* Firebase 参数放在 `firebase` 对象中：`{signer_key, salt_separator, rounds, mem_cost}`。
* `GET imports/` 列出已提交的批次。

## 文件格式

### JSON

一个人员数组，或一个包含 `users` 数组的对象。

| 字段 | 规则 |
| - | - |
| `email` | 必填。 |
| `external_id` | 最多 255 个字符。将成为该人员的 [`vendor_data`](https://docs.didit.me/zh/entities/vendor-data-linking)。 |
| `email_verified` | `true` 或 `false`（也接受 `1`/`0`、`yes`/`no`）。 |
| `display_name` | 最多 200 个字符。 |
| `password_hash`、`password_salt`、`password_algorithm` | 见[密码](#密码)。 |
| `totp_secret` | Base32 格式的身份验证器密钥。 |
| `created_at`、`last_signed_in_at` | ISO 8601 格式。不带时区的时间按 UTC 处理。 |
| `social` | 最多 8 个 `{provider, subject, tenant_id}`。`provider` 为 `google`、`apple`、`github` 或 `microsoft`。仅 Microsoft 需要 `tenant_id`。 |
| `organizations` | 最多 20 个 `{slug, name, role}`。`role` 为 `admin`、`member`（默认）或 `billing`。 |

### CSV

包含与上述单值字段相同的列，另外还有 `google_id`、`apple_id`、`github_id`、`microsoft_id`、`microsoft_tenant_id`、`organization_slug`、`organization_name` 和 `organization_role`。同一人属于多个组织时，每个组织占一行。

```csv theme={null}
email,external_id,email_verified,password_hash,google_id,organization_slug,organization_role
user@example.com,user-1234,true,$2b$12$...,1234567890,acme,admin
```

未知的字段或列会报错，绝不会被忽略。

### 重复数据与已有账户

* 邮箱相同的行会合并为一个人。完全相同的行被计为重复项。包含更多链接、组织或身份验证器的行会被合并。
* 同一人的两个不同密码哈希、身份验证器密钥或组织角色会报错。两个人声明同一 `external_id` 或同一提供方的 subject，同样会报错。
* 如果该邮箱的账户已存在，且是之前的导入创建的，则跳过该人员（`already_imported`）。如果该人员是自行注册的，则报 `identity_conflict` 错误。导入绝不会修改已有账户。

## 密码

| 格式 | 接受的条件 |
| - | - |
| bcrypt | `$2a$`、`$2b$` 或 `$2y$`，成本因子为 4 到 13。 |
| Argon2id | `$argon2id$v=19$...`。内存最多 64 MiB，内存乘以轮次最多 256 MiB，并行度最多为 8。 |
| Django PBKDF2-SHA256 | `pbkdf2_sha256$...`，迭代次数为 10,000 到 2,000,000。 |
| Django scrypt | `scrypt$...`，工作因子最多 32,768，块大小为 1 到 8，并行度为 1 到 5。 |
| Firebase scrypt | `password_algorithm: "firebase_scrypt"`，每位用户的 base64 `password_hash` 和 `password_salt`，另外每次导入时需提供项目的 `signer_key`、`salt_separator`、`rounds`（1 到 8）和 `mem_cost`（1 到 14）。 |

Firebase 参数会加密存储，且仅用于该应用。其他任何格式，或成本超出上述限制，都意味着该用户需要设置新密码。用户首次成功登录后，Didit 会以其自身格式重新存储该用户的密码。

### 没有可用密码的用户

当用户没有哈希值，或哈希值不受支持时，其密码登录尝试会收到常规的密码错误提示。Didit 还会向其发送一封邮件，其中包含设置新密码的链接（有效期 30 分钟，每天最多发送一次）。只有在该用户的邮箱已验证、账户未被封禁且已开启密码登录时，才会发送此邮件。

在结果中，`reset_required` 统计没有可用密码的用户数量。`reset_emails` 统计其中邮箱已验证的用户数量：只有这些用户可以通过这种方式联系到。邮箱未验证的用户无法使用密码登录。如果您启用了邮箱验证码登录方式，他们仍可以使用该方式登录。

## 社交账号关联、验证器应用和组织

* **社交账号关联**会保留给您的应用已配置的提供方，并沿用其当前的客户端 ID。之后再次登录时无需邮箱验证。Google 和 GitHub 的 subject 是全局唯一的。Apple 的 subject 按开发者团队区分，Microsoft 的 subject 按应用注册区分，因此只有当您的旧平台使用相同的团队或应用注册时，这些关联才会匹配。未配置的提供方的关联会计入 `social_unavailable`，且不会被导入。
* **验证器应用**（TOTP 密钥：SHA-1，6 位数字，30 秒）会加密存储。导入的用户必须将其作为第二因素使用。他们不会获得恢复码。
* 尚不存在的**组织**会根据其 slug 创建。
* **通行密钥**无法导入。它们属于您的旧登录域，因此用户需要在使用 Didit 登录后添加新的通行密钥。

## 结果

每次试运行以及每个批次和任务都会返回相同的 `result`：

| 字段 | 含义 |
| - | - |
| `rows`、`people_total` | 读取的行数和不重复的用户数。 |
| `ready`、`already_imported`、`duplicates`、`merged`、`skipped` | 每位用户的处理结果。 |
| `passwords` | 按格式统计的数量：`bcrypt`、`argon2id`、`firebase_scrypt`、`pbkdf2_sha256`、`scrypt`。 |
| `reset_required`、`reset_emails`、`unsupported_hashes` | 需要设置新密码的用户。 |
| `verified_emails` | 导入时邮箱已验证的用户数。 |
| `social_links`、`social_unavailable` | 按提供方保留的关联数，以及被跳过的关联数。 |
| `authenticator_apps` | 已导入验证器的用户数。 |
| `organizations` | `count`、`members`，以及最多五个 `names`。 |
| `error_count`、`errors` | 最多 100 条错误，每条包含从零开始计数的 `row`（CSV 行号为 `row + 2`）、`field` 和 `error` 错误代码。 |

响应中绝不包含密码哈希、盐值、验证器密钥或字段值。

| 错误 | 含义 |
| - | - |
| `file_too_large`、`too_many_rows`、`empty_file` | 超过 25 MB、超过 100,000 位用户，或文件为空。 |
| `invalid_encoding`、`invalid_json`、`invalid_csv` | 无法读取文件。 |
| `unknown_column`、`missing_column`、`duplicate_column`、`unknown_field` | 文件与格式不匹配。 |
| `required`、`invalid_field`、`invalid_row` | 某个值缺失或无效。 |
| `identity_conflict` | 使用该邮箱的账户已自行注册。 |
| `social_link_conflict`、`password_conflict`、`authenticator_conflict`、`role_conflict` | 同一用户的两行数据互相冲突。 |
| `too_many_uploads`、`import_running`、`not_startable`、`file_expired` | 当前无法启动任务。 |

## 切换前的检查

请在您的沙盒应用中进行以下测试：

* 使用您导入的每种密码格式登录，
* 没有可用哈希值的用户收到设置新密码的邮件，
* 导入用户的社交登录和验证器验证码，
* 对同一文件运行两次导入。

然后，检查每位用户的 `vendor_data` 是否与您用于其[验证和交易](https://docs.didit.me/zh/authentication/overview.md#在各产品间保持同一用户)的值一致。


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.