# Credential Management API

> navigator.credentials 的 get()、store()、create()、preventSilentAccess()：选项、mediation 模式、每个 DOMException 与浏览器缺口。

Credential Management API 就是 `navigator.credentials` 这个容器：页面通过它向浏览器的凭据存储索取一个 `Credential`、交回一个让浏览器保存、新建一个，或者告诉浏览器不要再静默登录。五种凭据类型接入这个容器：`PasswordCredential`、`PublicKeyCredential`（WebAuthn）、`OTPCredential`、`IdentityCredential`（FedCM），以及已被取代的 `FederatedCredential`。

容器本身历史久、覆盖广：`CredentialsContainer`、`get()` 与 `store()` 见于 Chrome 51、Edge 18、Firefox 60 与 Safari 13；`create()` 见于 Chrome 60；`preventSilentAccess()` 见于 Chrome 60（Chrome 51 至 59 叫 `requireUserMediation()`）、Edge 18、Firefox 60 与 Safari 17，而 Safari 13 至 16 暴露了该方法却对每次调用都以 `NotSupportedError` 拒绝（BCD `api.CredentialsContainer`）。凭据类型则窄得多：`PasswordCredential` 只存在于 Chrome 51，Firefox 与 Safari 任何版本都没有；`get({ otp })` 见于 Chrome 93（Android 上 Chrome 84）与 Safari 27；`get({ identity })` 见于 Chrome 108 与 Safari 27。本页讲容器；`PublicKeyCredential` 有独立条目。

## 语法

```js
navigator.credentials.get()
navigator.credentials.get(options)
navigator.credentials.store(credential)
navigator.credentials.create()
navigator.credentials.create(options)
navigator.credentials.preventSilentAccess()
```

`get()` 与 `create()` 返回 `Promise<Credential?>`：一个 `Credential` 子类实例，或者在 `mediation` 允许的用户介入程度内拿不到、建不出任何凭据时为 `null`。`store()` 与 `preventSilentAccess()` 返回 `Promise<undefined>`。四个方法都带 `[SecureContext]`；在 `localhost` 以外的 `http://` 源上容器为 `undefined`。

## 参数

`get()` 接受 `CredentialRequestOptions`，`create()` 接受 `CredentialCreationOptions`，`store()` 接受一个 `Credential`，`preventSilentAccess()` 不接受参数。每种凭据类型往两个字典里各加一个成员；一次请求通过包含哪些成员来声明它接受哪些类型。

| 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `mediation`（get、create） | `"silent"`、`"optional"`、`"conditional"` 或 `"required"` | 否 | 默认 `"optional"`。`"silent"` 不显示任何界面，直接以 `null` 兑现；`"optional"` 只在必要时显示选择器；`"conditional"` 把发现的凭据放进非模态界面（自动填充），并且不会很快以 `null` 兑现；`"required"` 忽略任何静默访问的许可，必定提示。对 `create()`，`"conditional"` 允许刚经过用户介入的登录免模态创建，否则拒绝。 |
| `uiMode`（get） | `DOMString` | 否 | `"immediate"`：有凭据则显示模态选择器，否则立即以 `null` 完成且不提示。 |
| `signal`（get、create） | `AbortSignal` | 否 | 中止时以该 signal 的 reason 拒绝（默认 `AbortError`，`AbortSignal.timeout()` 给出 `TimeoutError`），除非操作已经完成。 |
| `password`（get） | `boolean` | 否 | 默认 `false`。`true` 表示索取已保存的 `PasswordCredential`。 |
| `password`（create） | `PasswordCredentialData` 或 `HTMLFormElement` | 否 | `PasswordCredentialData` 含 `id`（必填）、`origin`（必填）、`password`（必填）、`name` 与 `iconURL`；传表单时按其 `autocomplete` 标记读取。 |
| `federated`（get、create） | `FederatedCredentialRequestOptions` / `FederatedCredentialInit` | 否 | `providers: sequence<USVString>` 与 `protocols: sequence<DOMString>`，用于已被取代的 `FederatedCredential`。 |
| `publicKey`（get、create） | `PublicKeyCredentialRequestOptions` / `PublicKeyCredentialCreationOptions` | 否 | WebAuthn；见「WebAuthn 与通行密钥」条目。 |
| `otp`（get） | `OTPCredentialRequestOptions` | 否 | `transport: ["sms"]`；兑现一个 `OTPCredential`，其 `code` 为短信一次性验证码。 |
| `identity`（get） | `IdentityCredentialRequestOptions` | 否 | `providers` 内含 `configURL`、`clientId` 及可选的 `nonce`、`loginHint`、`domainHint`；FedCM。 |

传给 `store()` 的 `credential` 必须是页面从 `get()` 得到的或自行构造的 `Credential`（`new PasswordCredential(data)` 或 `new PasswordCredential(form)`）。

## 异常

算法按固定顺序拒绝。凭据类型自身内部方法抛出的任何拒绝（例如 WebAuthn 超时的 `NotAllowedError`）原样透传。

| 异常 | 条件 |
|---|---|
| `InvalidStateError` | 文档不是 fully active（四个方法都适用）。Chromium 还在同一容器上另有请求进行中时使用它。 |
| `NotSupportedError` | `get()` 没有任何类型成员，或两个成员的类型不能共用一次请求；`create()` 带了多于一个类型成员，或没有关联文档。Safari 13 至 16 对每次 `preventSilentAccess()` 调用都以它拒绝，Chromium 在密码存储不可用时也以它拒绝。 |
| `SecurityError` | 从不透明源请求、创建或保存绑定源的类型（`PasswordCredential`、`FederatedCredential`）。 |
| `TypeError` | 对不支持的类型使用 `mediation: "conditional"` 或 `uiMode: "immediate"`；`create({ password })` 的数据或表单无法构造出 `PasswordCredential`。 |
| `NotAllowedError` | 请求的类型在当前 settings object 中已处于活动状态；该类型的 Permissions Policy 拒绝了文档；在与所有祖先不同源的上下文中收集或保存 `PasswordCredential`；没有事先同意就以 `mediation: "conditional"` 调用 `create()`。 |
| signal 的中止原因 | `get()` 或 `create()` 运行时 `options.signal` 已被中止，或在操作完成前被触发。 |

即使浏览器没有保存任何凭据，`preventSilentAccess()` 也会兑现；规范为它定义的唯一拒绝是 `InvalidStateError`。

:::observed
在 Chrome 中，第一个选择器仍打开时再次调用 `navigator.credentials.get({ password: true })`，以 `InvalidStateError: A request is already pending.` 拒绝；在密码存储无法打开的配置文件上，同一调用以 `NotSupportedError: The password store is unavailable.` 拒绝；并发的 FedCM `get({ identity })` 以 `NotAllowedError: Only one navigator.credentials.get request may be outstanding at one time.` 拒绝。三条字符串来自 Chromium [`authentication_credentials_container.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc)（chromium.googlesource.com）中的 `CredentialManagerErrorToDOMException` 与 `OnRequestToken`。
:::

## 示例

示例除了检测 `navigator.credentials`，还检测 `PasswordCredential`，因为 Firefox 60+ 与 Safari 13+ 提供容器却没有这个类型：`'credentials' in navigator` 为真，而 `get({ password: true })` 永远以 `null` 兑现（BCD `api.PasswordCredential`）。

### 进站时静默登录，带超时与可见的回退

`mediation: "silent"` 不显示选择器；需要用户同意时以 `null` 兑现，页面因此可以显示普通的登录按钮，而不是一个没人要的对话框。`AbortSignal.timeout()` 把卡住的存储变成可捕获的 `TimeoutError`。

```js
async function trySilentSignIn() {
  if (!('credentials' in navigator) || !('PasswordCredential' in window)) {
    showSignInButton();
    return;
  }
  try {
    const cred = await navigator.credentials.get({
      password: true,
      mediation: 'silent',
      signal: AbortSignal.timeout(5000),
    });
    if (!cred) return showSignInButton();
    await postJson('/session', { id: cred.id, password: cred.password });
    showSignedInState(cred.name ?? cred.id);
  } catch (err) {
    if (err.name === 'TimeoutError' || err.name === 'AbortError') return showSignInButton();
    throw err;
  }
}

window.addEventListener('load', trySilentSignIn);
```

`TimeoutError` 与 `AbortError` 都要分支处理：`AbortSignal.timeout()` 以前者拒绝，手动的 `AbortController` 以后者拒绝。

### 表单登录成功后记住密码

服务器接受凭据之后调用 `store()`，下次访问的静默 `get()` 才可能成功。用表单构造 `PasswordCredential`，让 `autocomplete="username"` 与 `autocomplete="current-password"` 字段提供 `id` 和 `password`。

```js
document.querySelector('#sign-in').addEventListener('submit', async (event) => {
  event.preventDefault();
  const form = event.currentTarget;
  const ok = await postForm('/session', new FormData(form));
  if (!ok) return showError('用户名或密码错误');

  if ('PasswordCredential' in window) {
    try {
      await navigator.credentials.store(new PasswordCredential(form));
    } catch (err) {
      console.warn(`store() failed: ${err.name}`); // 登录本身已经成功
    }
  }
  location.assign('/home');
});
```

只有 Chrome 51+ 会走进这个分支；在 Firefox 与 Safari 中表单正常提交，由浏览器自带的密码管理器提议保存。

### 登出时清除静默访问，兼顾 Safari 13 至 16

`preventSilentAccess()` 设置该源的标志，下一次 `get()` 就必须经过用户介入。在 Safari 13 至 16 上 `typeof` 检查能通过，但每次调用都以 `NotSupportedError` 拒绝，所以只捕获这一个名称并视为完成。

```js
async function signOut() {
  await fetch('/session', { method: 'DELETE' });
  if (typeof navigator.credentials?.preventSilentAccess === 'function') {
    try {
      await navigator.credentials.preventSilentAccess();
    } catch (err) {
      if (err.name !== 'NotSupportedError') throw err;
    }
  }
  location.assign('/');
}
```

只吞掉 `NotSupportedError`；文档已分离导致的 `InvalidStateError` 仍会抛出。

## 另请参阅

- [WebAuthn 与通行密钥（passkeys）](/zh/reference/capabilities/webauthn-passkeys/)，本页留给它的 `PublicKeyCredential` 类型
- [Payment Request API](/zh/reference/capabilities/payment-request/)，这类敏感步骤前适合先用 `mediation: "required"`
- [Web app manifest id：PWA 的稳定身份](/zh/reference/manifest/id/)，已安装应用自身的身份，而非用户的身份
- [Credential Management Level 1: Request a Credential](https://www.w3.org/TR/credential-management-1/#abstract-opdef-request-a-credential)（w3.org）
- [The Credential Management API](https://web.dev/articles/security-credential-management)（web.dev）
- [Chrome Platform Status: Credential Management API](https://chromestatus.com/feature/5026422640869376)（chromestatus.com）