# WebAuthn 与通行密钥（passkeys）

> Web Authentication API 与通行密钥如何为 PWA 提供抗钓鱼、无密码的登录——凭据创建与断言、可发现凭据、平台 vs 漫游认证器，以及跨设备同步。

**一句话：** Web Authentication API（WebAuthn）让站点注册并验证一个绑定到设备认证器的
公钥凭据，于是用户用指纹、面容或设备 PIN 登录，而非密码。**通行密钥（passkey）**是一种
可发现的 WebAuthn 凭据，通常会在用户的设备间同步——天生抗钓鱼，因为签名被绑定到站点的源
（origin），且私钥从不暴露给站点。

## WebAuthn 究竟做了什么

WebAuthn 用一对密钥取代共享密钥（密码）。对于通行密钥（可发现凭据），**私钥**由认证器
持有（安全元件、TPM 或平台密钥库）；而对于某些不可发现的凭据，私钥可能改由依赖方以包装
形式存储，用认证器持有的主密钥加密。无论哪种方式，**公钥**都由你的服务器存储，私钥从不
交给站点。有两种仪式（ceremony）：

- **注册**（`navigator.credentials.create()`）—— 认证器为你的源生成一对新密钥，返回公钥
  及一份证明（attestation）。你把公钥与凭据 ID 关联到用户账户存储。
- **认证**（`navigator.credentials.get()`）—— 认证器用私钥对服务器签发的挑战
  （challenge）进行签名。你的服务器用存储的公钥验证签名。

由于被签名的数据包含源与一个全新挑战，在仿冒域名上钓取到的凭据毫无用处，捕获的断言也无法
被重放。

## 通行密钥 vs "经典" WebAuthn

通行密钥是一种**可发现**（也叫驻留，resident）的 WebAuthn 凭据——认证器存储了足够的状态，
无需服务器先指明凭据 ID 即可找到该凭据。这正是"轻点即可登录"、无需先输入用户名的能力来源。
结合提供方同步（iCloud 钥匙串、Google 密码管理器等），同一个通行密钥可在用户的多台设备上
使用。

| | 经典 WebAuthn（第二因素） | 通行密钥 |
|---|---|---|
| 可发现凭据 | 通常不需要 | 需要（`residentKey: "required"`） |
| 是否先需用户标识 | 常需（用户名 + 密码，再用密钥） | 不需——凭据可发现 |
| 典型角色 | 密码之上的第二因素 | 主要的、无密码登录 |
| 跨设备 | 漫游安全密钥，逐设备 | 在用户设备间同步 |

## 平台 vs 漫游认证器

- **平台认证器** —— 内建于设备（Touch ID / Face ID、Windows Hello、Android 生物识别）。
  用 `authenticatorAttachment: "platform"` 请求它。最适合用户自有设备上的主通行密钥。
- **漫游（跨平台）认证器** —— 可移除的安全密钥（USB/NFC/蓝牙），或用一部手机登录到另一台
  设备。对硬件密钥或跨设备流程使用 `"cross-platform"`。

在提供通行密钥路径前，用
`PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()` 检测是否存在平台
认证器，并用 `isConditionalMediationAvailable()` 启用自动填充式的通行密钥建议。

## 条件式 UI（通行密钥自动填充）

以 `mediation: "conditional"` 调用 `navigator.credentials.get()`，可让浏览器在用户名
字段的自动填充中内联提供已保存的通行密钥，而非弹出模态框。该请求会静默等待，直到用户选中
一个通行密钥，因此你可以在页面加载时就发起它，又不打扰更愿意打字的用户。为输入框搭配
`autocomplete="username webauthn"`。

## 浏览器与生态支持

WebAuthn 在当前主流浏览器上获得支持。通行密钥——具备跨设备同步的可发现凭据——在若干主流
平台上可用，但覆盖仍在补齐：所引的 passkeys.dev 设备支持矩阵记录了各平台的空缺（例如，
在某些平台上，同步通行密钥被列为计划中而非已发布）。在受支持处，同步由平台凭据管理器
（例如 Google 密码管理器或 iCloud 钥匙串）处理，而非仅靠浏览器。由于该矩阵会变动，请特性
检测（`PublicKeyCredential`、条件式调解）而非假定。

## PWA 的职责

WebAuthn 的安全性取决于服务器端的验证。浏览器 API 交给你的是待校验的数据，而非一个完成的
决定：

- 为每次仪式生成一个加密随机的**挑战**并绑定到会话；拒绝陈旧或重用的挑战。
- 验证返回的客户端数据中的**源**与 **RP ID** 与你的站点匹配。
- 注册时存储公钥、凭据 ID 与**签名计数器**；每次认证时确认计数器没有倒退（克隆信号）。
- 把通行密钥当作账户凭据：允许用户登记不止一个、为其命名，并撤销丢失的那个。

## 实践清单

- [ ] 在展示任何通行密钥 UI 前特性检测 `window.PublicKeyCredential`。
- [ ] 用 `isUserVerifyingPlatformAuthenticatorAvailable()` 把守平台通行密钥路径。
- [ ] 为真正的通行密钥设置 `residentKey: "required"` 与 `userVerification: "preferred"`。
- [ ] 在服务器端签发全新随机挑战，并验证源、RP ID 与签名。
- [ ] 持久化并检查签名计数器，以检测被克隆的认证器。
- [ ] 在受支持处用 `autocomplete="username webauthn"` 提供条件式 UI 自动填充。
- [ ] 允许每账户多个通行密钥并提供恢复路径，使丢失设备不至于锁死账户。