# Credential Management API（凭据管理接口）

> navigator.credentials 如何创建、存储与获取登录凭据：四种凭据类型、mediation 模式，以及浏览器支持缺口。

**一句话：** 根据 MDN 的说明，Credential Management API "使网站能够创建、存储和获取
凭据"（"enables a website to create, store, and retrieve credentials"），核心接口是
`CredentialsContainer`，通过 `navigator.credentials` 暴露，并且只在安全上下文
（HTTPS）中可用。

## 它是什么

MDN 将凭据描述为"使系统能够做出身份验证决定的项目"——用户向网站出示的、用以证明
自己身份的凭证。核心接口 `CredentialsContainer` 提供三个主要方法：`create()`
（创建新凭据）、`store()`（在本地存储新凭据）以及 `get()`（获取凭据以登录用户）。

`CredentialsContainer` 的参考文档在这三个方法之外还列出了第四个实例方法，而它恰恰
最容易被忽略：`preventSilentAccess()`「设置一个标志，用于指定后续访问当前源时是否
允许自动登录，然后返回一个空的 `Promise`」。这四个方法都仅在安全上下文中可用。

这三个主要方法最好按它们各自 resolve 出什么来理解，因为形态并不相同：

- **`create()`** 以依据所给选项创建的新 `Credential` 实例 resolve——**或者在无法创建
  任何 `Credential` 对象时以 `null` resolve**。
- **`get()`** 以匹配所给参数的 `Credential` 实例 resolve。如果无法无歧义地取得单一
  凭据，则以 `null` resolve。
- **`store()`** 把一组用户凭据存入所提供的 `Credential` 实例，并在 `Promise` 中返回
  该实例。

根据 MDN，该 API 支持四种凭据类型，均为 `Credential` 的子类：

- **密码** —— `PasswordCredential`
- **联合身份** —— `IdentityCredential`（以及已弃用的 `FederatedCredential`）
- **一次性密码（OTP）** —— `OTPCredential`
- **Web Authentication** —— `PublicKeyCredential`

本页仅涵盖 `CredentialsContainer` 的通用能力；关于 WebAuthn/passkey 这一具体凭据
类型，请参阅专门的
[WebAuthn 与 passkey](/zh/reference/capabilities/webauthn-passkeys/) 参考文档。

## 支持情况

根据 MDN 针对 `CredentialsContainer` 的 browser-compat-data，该接口本身从 Chrome
51、Edge 18、Firefox 60 与 Safari 13 开始支持。具体方法的支持时间可能晚于接口
本身，因此应针对你实际需要的方法查阅对应的兼容性数据，而不要假设整个接口一次性
全部就绪。BCD 对同一接口的记录是：

| 方法 | Chrome | Edge | Firefox | Safari |
| --- | --- | --- | --- | --- |
| `CredentialsContainer` | 51 | 18 | 60 | 13 |
| `get()` | 51 | 18 | 60 | 13 |
| `store()` | 51 | `mirror` | 60 | 13 |
| `create()` | 60 | 18 | 60 | 13 |
| `preventSilentAccess()` | 60 | 18 | 60 | 17（13 为部分实现） |

`store()` 的 Edge 单元格写的是 `mirror`，因为 BCD 在那里记录的就是这个值：另外三个方法
各自带有一条显式的 Edge 记录（18），而 `store()` 没有自己的 Edge 数字，而是镜像对应的
Chromium 数据。

真正会咬人的是 `preventSilentAccess()` 的 Safari 单元格，因为它的故障模式并不是「方法不
存在」。BCD 为该方法记录了两条 Safari 条目：从 17 起完整支持，以及一条覆盖 **Safari 13 到
16** 的 `partial_implementation`，其说明写的是「该方法存在，但总是以 `NotSupportedError`
异常拒绝」。因此在这些版本上，`typeof navigator.credentials.preventSilentAccess ===
'function'` 这类检查会通过，调用却照样失败——方法确实存在，而且永久性损坏。要检测它，靠的
是捕获调用抛出的 `NotSupportedError`，而不是探测属性是否存在。

真正最容易让实际实现翻车的缺口，在更下一层——在凭据类型而不是容器上。MDN 把
`PasswordCredential` 标为**有限可用（limited availability）**：「该特性不属于
Baseline，因为它在一些使用最广泛的浏览器中并不工作。」BCD 则说明了具体是哪些：它
记录 `PasswordCredential` 自 Chrome 51 起支持，而在 Firefox 与 Safari 中记录为
*不支持*，两者给出的都是一个尚未关闭的实现缺陷链接，而不是版本号。因此
`'credentials' in navigator` 为真，并不能说明你可以构造 `PasswordCredential`：这两个
引擎提供了容器，却没有提供这种类型。

## 如何使用

有三个时刻值得关注：登录成功后存储、再次访问时取回，以及登出时清除自动登录标志。

```js
// 1. 在成功登录后存储一个密码凭据。
async function rememberPasswordCredential({ id, password, name, iconURL }) {
  if (!('PasswordCredential' in window)) return;
  const credential = new PasswordCredential({ id, password, name, iconURL });
  await navigator.credentials.store(credential);
}
```

`PasswordCredential` 以只读属性暴露 `password`、`name`（一个人类可读的字符串，提供
用于在凭据选择器中展示的公开名称）与 `iconURL`（指向一张图标图片的 URL），此外还有
它从 `Credential` 继承来的 `id` 与 `type`。

```js
// 2. 页面加载时尝试静默登录。
const credential = await navigator.credentials.get({
  password: true,
  mediation: 'silent',
});
if (credential) {
  await signInWith(credential);
} else {
  // 没有任何凭据能在不询问用户的前提下交出——那就展示登录按钮，
  // 而不是弹出一个用户并没有要求的对话框。
  showLoginButton();
}
```

```js
// 3. 登出时，阻止浏览器把用户直接再登回去。
async function signOut() {
  await serverSignOut();
  if (!navigator.credentials?.preventSilentAccess) return;
  try {
    await navigator.credentials.preventSilentAccess();
  } catch (err) {
    // Safari 13–16 提供了该方法，但总是以 NotSupportedError 拒绝。
    // 上面的属性检查看不出这一点，所以这里只吸收这一种拒绝，
    // 并退回到清理你自己的会话状态。
    if (err.name !== 'NotSupportedError') throw err;
    forgetLocalSession();
  }
}
```

这个 `try`/`catch` 会处理 Safari 13–16 的 `NotSupportedError`，因为存在性检查无法区分一个
可用的方法和 BCD 记录的那种部分实现。注意这里只吞掉了 `NotSupportedError`——其他异常仍会
继续抛出，真正的故障不会被藏起来。

MDN 给出的正是这个理由：「你可以在用户登出网站之后调用它，以确保他们在下次访问站点
时不会被自动登录。」该方法以 `undefined` resolve。

### 选择让用户参与到什么程度

`get()` 接受一个 `mediation` 选项——一个字符串，表示在取回凭据的过程中用户以何种方式
参与。默认值是 `"optional"`。根据 MDN，四个取值分别是：

- **`"silent"`** —— 不会要求用户进行身份验证；用户代理会在可能的情况下自动重新验证并
  登录用户，而**如果需要用户同意，该 promise 会以 `null` 兑现**。适用于希望用户一进入
  应用就尽可能自动登录，但在做不到时也不要弹出令人困惑的登录对话框的场景。
- **`"optional"`** —— 如果凭据能在无需用户介入的情况下交出，就直接交出，从而实现自动
  重新验证；如果需要用户介入，用户代理则会要求用户进行身份验证。适用于你有合理把握
  用户不会因为看到登录对话框而感到意外的场景——例如用户刚点击了「登录/注册」按钮。
- **`"required"`** —— 总是要求用户进行身份验证。适用于你想强制身份验证的场景，例如在
  执行敏感操作前要求重新验证（MDN 给的例子是确认一笔信用卡付款），或在切换用户时。
- **`"conditional"`** —— 已发现的凭据会以非模态对话框的形式呈现给用户，同时标明请求
  凭据的来源。实践中这意味着对可用凭据进行自动填充。

`get()` 另外两个值得了解的选项：`password` 是那个布尔值，用于请求浏览器以
`PasswordCredential` 形式取回已存储的密码；`federated` 接受
`{ protocols, providers }`——但 MDN 指出 `FederatedCredential`「现已被取代，开发者
应优先使用 `identity` 选项（如果可用）」。

### 给请求设定一个期限

`get()` 接受 `signal`——一个 `AbortSignal`，可用于中止一个正在进行的请求。被中止的
操作可能仍正常完成（通常是因为中止发生在操作已经结束之后），也可能以该 signal 的
reason 拒绝，默认是一个 `AbortError` `DOMException`。把它与 `AbortSignal.timeout()`
搭配，就能把一个无人应答的提示变成可捕获的错误：

```js
try {
  const credential = await navigator.credentials.get({
    password: true,
    signal: AbortSignal.timeout(10_000),
  });
  // …
} catch (err) {
  if (err.name === 'TimeoutError') return showLoginButton();
  if (err.name === 'AbortError') return; // 请求被取消
  throw err;
}
```

因 `AbortSignal.timeout()` 设定的超时而被自动中止的请求，拒绝时给出的是
`TimeoutError` 而不是 `AbortError`——如果你用了期限，请对两者都做分支处理。

## 如何在运行时检测

在调用之前先对 `navigator.credentials` 做特性检测，因为在不支持该 API 的浏览器
或上下文（例如非安全源）中，它是未定义的。又因为容器可能存在、而你需要的凭据类型
并不存在，所以两者都要检测：

```js
async function getStoredCredential() {
  if (!('credentials' in navigator) || !('PasswordCredential' in window)) {
    // 要么此处无法使用 Credential Management API（浏览器不支持，或处于
    // 非安全上下文），要么该引擎只提供了容器而没有密码凭据——
    // 回退到手动登录表单。
    return null;
  }
  return navigator.credentials.get({ password: true, mediation: 'silent' });
}
```

## 实践清单

- 根据 MDN，该 API 仅在安全上下文（HTTPS）中可用——在依赖它之前，请确认页面是
  通过安全方式提供的。
- 不要把 `'credentials' in navigator` 当作密码凭据可用的证据：BCD 记录
  `PasswordCredential` 在 Firefox 与 Safari 中不受支持，而这两者都提供了
  `CredentialsContainer`。要检测的是凭据类型，而不只是容器。
- 把 resolve 出 `null` 当作正常结果而非错误：无法无歧义取得单一凭据时 `get()` 以
  `null` resolve；需要用户同意时 `mediation: 'silent'` 以 `null` 兑现；无法创建任何
  `Credential` 对象时 `create()` 以 `null` resolve。
- MDN 在四种凭据类型中将 `FederatedCredential` 标记为已弃用，并指出 `federated`
  选项已被 `identity` 选项取代（在可用的情况下）——在基于它构建新的登录流程之前，
  请查阅 MDN 上的最新状态。
- 各方法的支持可能滞后于接口本身：BCD 把 `create()` 与 `preventSilentAccess()` 记在
  Chrome 60，而容器本身是 Chrome 51；`preventSilentAccess()` 记在 Safari 17，而容器
  是 Safari 13。请针对你实际需要的方法逐一确认。
- 不要用检查属性的方式来对 `preventSilentAccess()` 做特性检测。BCD 把 Safari 13–16 记录
  为部分实现：方法存在，但总是以 `NotSupportedError` 拒绝，所以检查会通过而调用照样失败。
  要把调用包起来，捕获 `NotSupportedError` 后退回到清理你自己的会话，并让其他异常继续抛出。
- 在登出时调用 `preventSilentAccess()`，而不是在登录时——它正是那个阻止下次访问把
  用户直接登回去的标志位。注意对于 `PublicKeyCredential`，它通常不起作用，因为这类
  验证器通常本就需要用户交互。
- 如果你在读更老的代码或更老的文章：在早期版本的规范中，`preventSilentAccess()` 叫
  `requireUserMediation()`。
- 对 `get()` 的各类拒绝分别处理：`NotAllowedError` 覆盖用户取消请求、调用被
  `identity-credentials-get`、`publickey-credentials-get` 或 `otp-credentials` 权限
  策略阻止，以及调用方来源是不透明源（opaque origin）；`SecurityError` 表示调用方
  域名不是有效域名；`AbortError` 与 `TimeoutError` 来自 `signal` 选项。
- 对于 `PublicKeyCredential`（WebAuthn/passkey）这一具体类型，请遵循下方专门的
  WebAuthn 参考文档，而不要将其当作通用凭据处理。

## 延伸阅读

- [WebAuthn 与 passkey](/zh/reference/capabilities/webauthn-passkeys/) ——
  `PublicKeyCredential` 类型的深入说明，也是本页唯一留给专门文档的凭据类型。
- [Web 能力索引](/zh/reference/capabilities/) —— PWA 可以依托的其他浏览器能力。
- [Payment Request API](/zh/reference/capabilities/payment-request/) —— MDN 为
  `mediation: "required"` 举例时所说的那类敏感操作。
- [Web app manifest id：稳定的 PWA 身份标识](/zh/reference/manifest/id/) ——
  已安装应用自身的身份标识，与用户的身份相对。