指南/第 08 章

CHAPTER 08 / 12

现代 Passkey 补充:同步、条件式 UI 与能力检测

本章明确区分历史规范与后续实践:前面的核心协议以 WebAuthn Level 2 为主;这里补充 Passkey 生态和 Level 3 内容。截至本次核对的 2026 年 10 月 16 日,W3C 的 Level 3 发布版标示为 2026 年 8 月 25 日 Recommendation。标准发布并不等于每个浏览器已实现全部功能。

同步改变的是凭据可用性#

Passkey 可以保存在单个设备,也可以由凭据提供方安全同步到其他设备。对网站而言,核心仍是登记公钥、验证签名;“可发现”描述如何选择凭据,“同步”描述凭据能否跨设备可用,两者不是同一个维度。FIDO Alliance:Passkeys

不要将“用了平台认证器”推导为“已同步”,也不要将“安全密钥”推导为“用户无需 PIN”。注册策略应表达实际需要:可发现性用 residentKey,本地用户验证用 userVerification。选择具体设备、提供方和恢复方式则由产品目标及用户环境共同决定。

跨设备认证也不等于同步。用户可能在电脑上发起登录,再用手机中的凭据完成认证;网站并没有因此把私钥复制到该电脑。具体选项与提示由浏览器和平台决定,应保留取消与替代路径。

BE 与 BS:两个备份标志#

Level 3 为认证器数据补充了备份语义:

标志 含义 不应推导出的结论
BE,Backup Eligibility 凭据是否具有可备份资格;创建后保持不变 不能据此断言用户已经在另一设备上可用
BS,Backup State 当前是否处于已备份状态;可能变化 不能证明用户现实身份,也不保证永久可恢复

英文原文:Level 3 §6.1.3 Credential Backup State。示例使用验证库给出的 credentialDeviceType 与 credentialBackedUp 保存相关信息;它们适合辅助账户恢复策略和界面提示,不应替代验签。

计数器不能承担完整的克隆检测#

有些认证器一直返回零;不递增也可能来自故障或请求处理乱序,不能单凭一次异常认定克隆。对现代多设备凭据,更不能自行加上一条“所有登录计数必须严格大于零”的规则。风险处置应结合凭据属性和业务情境。Level 3 §6.1.1 Signature Counter Considerations

配套示例直接使用已锁定版本验证库的策略:零对零通过,非零回退拒绝;在单进程内为读、验、写加锁。若你的生产政策不同,应选择并审查支持相应行为的库或明确实现路径,不能为了绕过错误而把数据库计数器无条件清零。

条件式 UI:在输入框中选择通行密钥#

条件式 UI(conditional mediation)让登录页的自动填充菜单显示可用通行密钥。页面预先挂起 get() 请求;用户选择凭据后才完成流程。它是按钮登录之外的增强,需先检测能力,并保留按钮登录入口。web.dev:Passkey 自动填充指南

输入框需要 webauthn 自动填充标记,放在 token 列表最后:

html
<label for="username">账号</label>
<input id="username" name="username"
       autocomplete="username webauthn">

下面是在本地示例基础上扩展的说明片段;默认下载示例只实现按钮登录,不会自动发起条件式请求。它复用 browser.js 的 JSON 转换函数:

js
import { authenticationOptions, credentialJSON } from './browser.js';

const controller = new AbortController();

async function conditionalLogin() {
  const available = window.PublicKeyCredential &&
    await PublicKeyCredential.isConditionalMediationAvailable?.();
  if (!available) return; // 保留已有按钮登录

  const optionResponse = await fetch('/api/authentication/options', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: '{}',
  });
  if (!optionResponse.ok) throw new Error('无法开始登录');
  const options = await optionResponse.json();

  const credential = await navigator.credentials.get({
    publicKey: authenticationOptions(options),
    mediation: 'conditional',
    signal: controller.signal,
  });
  if (!credential) return;

  const verification = await fetch('/api/authentication/verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(credentialJSON(credential)),
  });
  if (!verification.ok) throw new Error('验证失败,请重新开始');
  // 现在可以刷新账户页面;不能仅根据 get() 返回值显示已登录。
}

conditionalLogin().catch((error) => {
  if (error.name !== 'AbortError' && error.name !== 'NotAllowedError') {
    console.error('条件式登录未完成', error);
  }
});

// 用户点击显式登录 / 注册按钮时,先调用 controller.abort(),
// 再申请新的 options,避免与挂起请求争用同一 challenge。

挂起时间可能超过服务器的 challenge 有效期。接入时应设计重新申请与重试流程;不要无限延长 challenge 以适配长时间打开的标签页。一个页面也应协调好注册、条件式请求和显式登录,避免多个流程互相覆盖。

若使用 @simplewebauthn/browser,当前文档中的封装形式是 startAuthentication({ optionsJSON, useBrowserAutofill: true }),仍需合适的输入框和服务端验证。不要混用旧文章中的位置参数签名。SimpleWebAuthn Browser 官方文档

新版 JSON API 与渐进增强#

Level 3 增加了创建 / 请求选项的 JSON 解析和凭据 JSON 序列化能力。是否采用它们应逐项检测;没有能力时仍可使用示例中的显式转换,不要依据浏览器名称或版本字符串猜测。

js
const publicKey = typeof PublicKeyCredential.parseRequestOptionsFromJSON === 'function'
  ? PublicKeyCredential.parseRequestOptionsFromJSON(optionsJSON)
  : authenticationOptions(optionsJSON);

这些方法改变数据转换方式,没有取消服务端验证或改变账号授权边界。Level 3:PublicKeyCredential 接口

继续阅读:从示例到生产系统。

读懂原理之后,用示例验证你的理解。

搜索指南

输入关键词,查找你想了解的内容。

搜索覆盖全部 12 章 · 点击结果定位到相关段落
学习目录