# WebAuthn 与通行密钥（passkeys）

> navigator.credentials.create() 与 get() 搭配 publicKey 注册并验证通行密钥：每个选项成员、各个 DOMException，以及可运行的登录示例。

Web Authentication API 通过 `navigator.credentials.create({ publicKey })` 注册一个绑定到认证器的公钥凭据，之后用 `navigator.credentials.get({ publicKey })` 证明持有它，于是 PWA 可以让用户用指纹、人脸或设备 PIN 登录，而不是密码。通行密钥（passkey）就是以可发现方式（`residentKey: "required"`）创建的这类凭据，在多数平台上由 iCloud 钥匙串或 Google 密码管理器等凭据管理器同步；签名数据包含源与一次性 challenge，所以在仿冒域名上钓到的凭据无法使用。

`PublicKeyCredential` 在 Chrome 67、Edge 18、Firefox 60 与 Safari 13 发布；条件式中介（通行密钥自动填充）在 Chrome 108、Safari 16 与 Firefox 119 可用；`PublicKeyCredential.getClientCapabilities()` 在 Chrome 133、Safari 17.4 与 Firefox 135 可用（BCD `api.PublicKeyCredential`）。Android WebView 131 暴露 `isConditionalMediationAvailable()`，但由于缺少自动填充集成，它兑现为 `false`。各平台的通行密钥同步覆盖情况见 [passkeys.dev](https://passkeys.dev/device-support/)。

## 语法

```js
navigator.credentials.create({ publicKey: creationOptions, signal })
navigator.credentials.get({ publicKey: requestOptions, mediation, signal })

PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()
PublicKeyCredential.isConditionalMediationAvailable()
PublicKeyCredential.getClientCapabilities()
PublicKeyCredential.parseCreationOptionsFromJSON(json)
PublicKeyCredential.parseRequestOptionsFromJSON(json)
credential.toJSON()
```

`create()` 以一个 `PublicKeyCredential` 兑现，其 `response` 是 `AuthenticatorAttestationResponse`（`clientDataJSON`、`attestationObject`、`getPublicKey()`、`getTransports()`）；`get()` 兑现的凭据其 `response` 是 `AuthenticatorAssertionResponse`（`clientDataJSON`、`authenticatorData`、`signature`、`userHandle`）。静态的 `is…Available()` 方法以布尔值兑现，不需要手势。两个仪式都要求安全上下文；跨源 iframe 还需要 `publickey-credentials-create` 或 `publickey-credentials-get` Permissions Policy 特性。

## 参数

`create()` 在 `publicKey` 下接受 `PublicKeyCredentialCreationOptions`；`get()` 接受 `PublicKeyCredentialRequestOptions`，再加上外层 `CredentialRequestOptions` 的 `mediation` 成员。二进制成员的类型是 `BufferSource`；JSON 变体接受 base64url 字符串。

| 字典 | 成员 | 类型 | 必填 | 说明 |
|---|---|---|---|---|
| 创建 | `rp` | `PublicKeyCredentialRpEntity` | 是 | `name`（必填）与 `id`（默认为源的有效域名，必须是它的可注册后缀）。 |
| 创建 | `user` | `PublicKeyCredentialUserEntity` | 是 | `id`（1 到 64 字节，不透明）、`name`、`displayName`。 |
| 创建 | `challenge` | `BufferSource` | 是 | 服务端生成的随机字节，回显在 `clientDataJSON` 中。 |
| 创建 | `pubKeyCredParams` | `sequence<PublicKeyCredentialParameters>` | 是 | `{ type: "public-key", alg }` 条目；`-7`（ES256）与 `-257`（RS256）覆盖当前的认证器。 |
| 创建 | `timeout` | `unsigned long` | 否 | 毫秒提示值，客户端可能裁剪。 |
| 创建 | `excludeCredentials` | `sequence<PublicKeyCredentialDescriptor>` | 否 | 用户已有的凭据 ID；匹配到的认证器以 `InvalidStateError` 拒绝。 |
| 创建 | `authenticatorSelection` | `AuthenticatorSelectionCriteria` | 否 | `authenticatorAttachment`（`"platform"` 或 `"cross-platform"`）、`residentKey`（`"discouraged"`、`"preferred"`、`"required"`）、`requireResidentKey`（遗留布尔值）、`userVerification`（`"required"`、默认 `"preferred"`、`"discouraged"`）。 |
| 创建 | `hints` | `sequence<DOMString>` | 否 | `"security-key"`、`"client-device"`、`"hybrid"`，按偏好排序。 |
| 创建 | `attestation` | `DOMString` | 否，默认 `"none"` | `"none"`、`"indirect"`、`"direct"`、`"enterprise"`。 |
| 创建 | `extensions` | `AuthenticationExtensionsClientInputs` | 否 | 例如 `credProps`，用于得知凭据是否成为可发现凭据。 |
| 请求 | `challenge` | `BufferSource` | 是 | 新的服务端 challenge。 |
| 请求 | `rpId` | `DOMString` | 否 | 必须与创建时使用的 `rp.id` 一致。 |
| 请求 | `allowCredentials` | `sequence<PublicKeyCredentialDescriptor>` | 否 | 留空让认证器自行选择可发现凭据（通行密钥流程）。 |
| 请求 | `userVerification` | `DOMString` | 否，默认 `"preferred"` | 同上。 |
| 请求 | `timeout`、`hints`、`extensions` | | 否 | 与创建时相同。 |
| 外层 | `mediation` | `CredentialMediationRequirement` | 否，默认 `"optional"` | `"conditional"` 在表单自动填充中展示通行密钥并静默等待；`"required"` 强制弹窗；`"silent"` 对 `publicKey` 会被拒绝。 |
| 外层 | `signal` | `AbortSignal` | 否 | 中止后，进行中的仪式以 `AbortError` 拒绝。 |

## 异常

| 异常 | 条件 |
|---|---|
| `NotAllowedError` | 调用方的源是不透明源；文档与某个祖先跨源且缺少对应的 Permissions Policy 特性；客户端要求瞬时激活而调用时没有；用户拒绝、超时，或找不到可用凭据且用户关闭了对话框；或（`create()`）用户代理近期没有中介过认证且不授予同意。 |
| `SecurityError` | 有效域名不是合法域名，或 `rp.id` / `rpId` 既不等于它也不是它的可注册后缀，且关联源检查（`/.well-known/webauthn`）失败。 |
| `TypeError` | `user.id` 不在 1 到 64 字节之间。 |
| `NotSupportedError` | `pubKeyCredParams` 中没有任何一项的类型与算法被客户端支持。 |
| `InvalidStateError` | 某个认证器持有 `excludeCredentials` 中列出的凭据且用户已同意（这是客户端唯一原样上抛的认证器错误）。 |
| `AbortError` | `signal` 已中止。 |
| `EncodingError` | `parseCreationOptionsFromJSON()` 或 `parseRequestOptionsFromJSON()` 遇到无法解码的值。 |

`InvalidStateError` 以外的认证器级状态（认证器没有可发现凭据存储以满足 `residentKey: "required"`、或不具备用户验证能力时的 `ConstraintError`，内部失败时的 `UnknownError`）不会被规范的客户端算法上抛；仪式继续尝试其他认证器，最终在超时时以 `NotAllowedError` 结束。Chrome 与此不同，会直接报告这些状态（见实测）。

:::observed
Chrome 在 [`authentication_credentials_container.cc`](https://chromium.googlesource.com/chromium/src/+/main/third_party/blink/renderer/modules/credentialmanagement/authentication_credentials_container.cc)（chromium.googlesource.com）中把认证器结果映射为固定文案：关闭通行密钥对话框或任其超时，以 `NotAllowedError: The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.` 拒绝；`rp.id` 不是源的后缀时以 `SecurityError: This is an invalid domain.` 拒绝；命中 `excludeCredentials` 时以 `InvalidStateError: The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.` 拒绝；标签页未获焦点时调用以 `NotAllowedError: The operation is not allowed at this time because the page does not have focus.` 拒绝。DevTools 中名为 **WebAuthn** 的面板（英文界面，位于 More tools 下）可添加虚拟认证器，无需硬件即可走通这些路径（[Chrome DevTools: Emulate authenticators](https://developer.chrome.com/docs/devtools/webauthn)（developer.chrome.com））。
:::

## 示例

每个示例都先检测 `PublicKeyCredential`，并给出在没有通行密钥的环境里保留密码登录的分支。challenge、用户 ID 与凭据 ID 由服务端以 base64url 字符串下发，浏览器有 JSON 辅助方法时直接交给它们处理。

### 注册通行密钥，缺少支持时回退到密码表单

向服务端请求创建选项，确认存在平台认证器，再把证明（attestation）回传。Chrome 129、Firefox 119 与 Safari 18.4 之前的浏览器没有 `parseCreationOptionsFromJSON()`，在那里手动解码 base64url。

```js
const fromB64url = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));

async function registerPasskey() {
  if (!window.PublicKeyCredential) return showPasswordForm('当前浏览器不支持通行密钥。');
  if (!(await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable())) {
    return showPasswordForm('此设备没有平台认证器。');
  }

  const json = await (await fetch('/webauthn/register/options', { method: 'POST' })).json();
  const publicKey = PublicKeyCredential.parseCreationOptionsFromJSON
    ? PublicKeyCredential.parseCreationOptionsFromJSON(json)
    : {
        ...json,
        challenge: fromB64url(json.challenge),
        user: { ...json.user, id: fromB64url(json.user.id) },
        excludeCredentials: (json.excludeCredentials ?? []).map((c) => ({ ...c, id: fromB64url(c.id) })),
      };

  try {
    const credential = await navigator.credentials.create({ publicKey });
    await fetch('/webauthn/register/verify', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(credential.toJSON ? credential.toJSON() : serialize(credential)),
    });
  } catch (err) {
    if (err.name === 'InvalidStateError') return notify('此设备上已有该账号的通行密钥。');
    if (err.name === 'NotAllowedError') return notify('已取消创建通行密钥。');
    throw err;
  }
}
```

服务端的选项应设置 `authenticatorSelection: { residentKey: "required", userVerification: "preferred" }`，凭据才是可发现的；否则用户必须先输入用户名，`get()` 才找得到它。

### 用通行密钥自动填充登录，并保留弹窗回退

`mediation: "conditional"` 让浏览器把通行密钥列在用户名输入框的自动填充里。请求会一直等到用户选中，所以在页面加载时就发起，并用一个 `AbortSignal` 配合可见的「用通行密钥登录」按钮：按钮先取消它，再发起弹窗请求。

```js
async function startConditionalSignIn(input, button) {
  if (!window.PublicKeyCredential?.isConditionalMediationAvailable) return;
  if (!(await PublicKeyCredential.isConditionalMediationAvailable())) return;

  input.setAttribute('autocomplete', 'username webauthn');
  const controller = new AbortController();
  const options = await (await fetch('/webauthn/login/options')).json();
  const publicKey = PublicKeyCredential.parseRequestOptionsFromJSON(options);

  button.addEventListener('click', async () => {
    controller.abort();
    const assertion = await navigator.credentials.get({ publicKey, mediation: 'required' });
    await verify(assertion);
  });

  try {
    const assertion = await navigator.credentials.get({ publicKey, mediation: 'conditional', signal: controller.signal });
    await verify(assertion);
  } catch (err) {
    if (err.name !== 'AbortError') console.error(`${err.name}: ${err.message}`);
  }
}
```

把 `allowCredentials` 留空，这样为 `rpId` 保存的任何通行密钥都能出现；没有通行密钥的用户看到的是普通自动填充，密码路径不受影响。

### 在平台通行密钥与安全密钥之间取舍

`hints` 告诉浏览器优先展示哪种界面。给员工配发硬件密钥的企业应用，以 `"security-key"` 开头并要求用户验证；面向消费者的应用用 `"client-device"`，让设备自带的生物识别排在最前。

```js
async function createFor(audience, publicKey) {
  if (!window.PublicKeyCredential) throw new Error('WebAuthn 不可用');
  const options = {
    ...publicKey,
    hints: audience === 'workforce' ? ['security-key', 'hybrid'] : ['client-device', 'hybrid'],
    authenticatorSelection: {
      residentKey: 'required',
      userVerification: audience === 'workforce' ? 'required' : 'preferred',
    },
  };
  return navigator.credentials.create({ publicKey: options });
}
```

不支持 `hints` 的浏览器按 WebIDL 字典规则丢弃该成员，回退是自动的：显示默认选择器。

## 另请参阅

- [Credential Management API（凭据管理接口）](/zh/reference/capabilities/credential-management/)，这些调用所在的容器
- [iOS 与 Safari 上的 PWA](/zh/reference/platforms/ios-safari/)，通行密钥在那里经 iCloud 钥匙串同步
- [Web Authentication Level 3: create a new credential](https://www.w3.org/TR/webauthn-3/#sctn-createCredential)（w3.org）
- [Web Authentication Level 3: get an assertion](https://www.w3.org/TR/webauthn-3/#sctn-getAssertion)（w3.org）
- [Create a passkey for passwordless logins](https://web.dev/articles/passkey-registration)（web.dev）
- [Sign in with a passkey through form autofill](https://web.dev/articles/passkey-form-autofill)（web.dev）
- [Device support for passkeys](https://passkeys.dev/device-support/)（passkeys.dev）