THE COMPLETE GUIDE · 全文版

Passkey 中文开发指南

以 W3C WebAuthn Level 2 为基础,面向开发实践的中文编译学习文档。非官方译文;现代 Passkey 补充已标注。

下载 Markdown ↓
  1. 从英文规范开始,学会 Passkey 开发
  2. 术语与安全边界
  3. 注册与认证:把信任链串起来
  4. 注册详解:安全地绑定一把新公钥
  5. 认证详解:从签名到可信登录会话
  6. 服务端验证:安全判断发生在哪里
  7. 完整示例:本地注册与无用户名登录
  8. 现代 Passkey 补充:同步、条件式 UI 与能力检测
  9. 从学习示例到生产系统
  10. 调试、错误与测试
  11. 英文规范导读与章节对照
  12. 来源、版本与译注

CHAPTER 01

从英文规范开始,学会 Passkey 开发

Passkey(通行密钥)让用户通过设备上的解锁方式登录网站。对开发者来说,核心变化是:服务器保存公钥,登录时验证一次性挑战的签名,而不是接收用户输入的密码。指纹、面容或设备 PIN 用于本地授权,不会作为 WebAuthn 响应发送给网站。

这份文档面向已经了解 HTTP、JavaScript 和基本登录流程的开发者。目标是读懂 WebAuthn 的安全边界,理解注册与登录的数据流,并通过一个可运行的示例把浏览器 API 和服务器验证连接起来。

这份中文文档与英文原文的关系#

你提供的链接是 W3C 的 Web Authentication Level 2,发布于 2021 年 4 月 8 日。它是一份规范,涉及浏览器、认证器和网站服务器的行为;它并非一篇按现代 Passkey 产品体验组织的开发教程。

本书将原规范中与网站开发最相关的部分重新编排,提供中文整理、关键语义译解、字段说明及新增代码讲解。这是非官方的学习版本,不是逐条完整译本;原规范的算法细节、符合性条款和所有证明格式仍应查阅英文。正文中的“译解”保留原意,其他解释、示例、取舍建议属于本书补充。出处、修改说明和文档使用规则见参考资料与版权说明。

截至本书核对日期 2026 年 10 月 16 日,WebAuthn Level 3 已于 2026 年 8 月 25 日成为 W3C Recommendation。下文以你指定的 Level 2 为基础;同步通行密钥、条件式界面和新的 JSON 辅助方法等现代内容,会明确标为补充,避免把后来的能力误认为 2021 年原文已有的要求。

先理解三个层次#

层次 解决的问题 你主要接触的内容
Passkey 用户体验 如何让用户创建、选择、使用通行密钥 注册入口、系统选择器、账户恢复、凭据管理
WebAuthn 网页如何请求创建凭据、如何获得认证断言 navigator.credentials.create()、navigator.credentials.get()
认证器与通信机制 私钥由谁保护、如何与客户端协作 平台认证器、安全密钥;外部认证器可能通过 CTAP 等机制通信

不要把“Passkey”理解为某个 JavaScript 库,也不要把“FIDO2”理解为一条独立的 HTTP 登录接口。WebAuthn 负责浏览器侧的标准接口和相关协议要求;你仍然要实现服务器端的挑战管理、响应验证和会话管理。

FIDO 对 Passkey 的说明包含同步型和设备绑定型凭据。是否能够同步、同步到哪些设备、如何恢复,是凭据提供者与平台能力的一部分;网站不能仅凭浏览器 create() 成功,就推断凭据一定已经备份到云端。FIDO 官方 Passkey 介绍

原规范导读:两种 ceremony#

“Ceremony”在这里译为“交互流程”:它不仅是一次密码学运算,还包括服务器请求、浏览器判断来源、认证器与用户交互以及服务器最终校验。

**§1 引言译解:**网站通过两种相关流程使用 WebAuthn。注册时,认证器为该网站创建公钥凭据,并把它关联到当前用户的账户;认证时,网站接收认证断言,以验证注册该凭据的用户参与并同意了本次操作。英文原文 §1 Introduction

把它改写成开发任务:

  1. 注册:先确认“给哪个账户添加凭据”,再请求生成密钥;验证响应后保存凭据 ID、公钥与账户的关系。
  2. 登录:先发出新的随机挑战,再验证已有私钥对本次上下文的签名;验证全部通过后建立登录会话。

这里的“注册凭据”不等于“新建用户”。一个已有账户可以添加多个凭据;用户也可以先创建账户,再完成通行密钥注册。网站必须确定这两步之间的可信账户绑定,不能任由客户端在完成注册时提交另一个账户 ID。

建议学习路线#

阶段 阅读内容 完成后应能解释
建立模型 术语与安全边界、注册与认证协议 RP ID、origin、UP/UV,以及谁持有私钥
接入浏览器 注册详解、认证详解 创建与获取凭据所需字段,JSON 与二进制转换
接入服务器 服务端验证章节与完整示例 为什么不能仅凭浏览器返回成功就登录
上线前检查 安全、恢复、测试与部署章节 挑战的一次性、域名绑定、账户恢复与真实设备验证
深入规范 原文 §6、§8、§9、§10 认证器数据结构、证明格式和扩展机制

如果你是在已有网站中增加登录方式,优先完成“已登录用户添加凭据 → 验证通行密钥登录 → 凭据管理与恢复”这条闭环。这样可以先解决账户绑定,再研究无用户名登录和自动填充等交互。

一个最小心智模型#

假设网站为 https://login.example.com,用户是“小林”。

注册后,服务器保存类似以下信息:

text
账户: user_42
用户句柄: 随机的、不含邮箱的字节序列
凭据 ID: 认证器生成的凭据标识
凭据公钥: 与该凭据对应的验证公钥
计数器等状态: 验证器返回并按策略维护

再次登录时,服务器生成随机挑战 challenge_A。浏览器请求认证器对绑定到该网站的凭据进行操作。认证器授权后产生签名;服务器使用之前保存的公钥,连同挑战、来源、RP ID 等一起验证。

如果攻击者复制上一轮响应,challenge_A 已失效,不能用于新一轮登录。如果攻击者搭建另一个域名的钓鱼网站,标准客户端会根据调用来源限制它可使用的 RP ID。防重放依赖服务器正确管理挑战,防钓鱼依赖完整执行来源与 RP 绑定校验,两者都不能靠前端 UI 替代。

Passkey 没有替你完成什么#

你仍然需要决定“谁能添加或删除一个凭据”“丢失所有凭据后如何恢复”“成功认证后如何保护会话”。通行密钥减少密码泄露、复用和钓鱼风险,但恶意脚本、会话窃取、薄弱的账户恢复流程和错误的服务器验证仍可能让账户被接管。

静态文档可以发布到 Cloudflare Pages,浏览器也能在 HTTPS 页面调用 WebAuthn;真正的多用户登录还需要可信服务器或边缘函数、持久化存储和安全会话。本书中的学习网站与可运行示例会清楚区分这两个层次。

读原文时的定位方法#

W3C 原文会用 MUST、SHOULD、MAY 区分要求强度。可分别理解为“必须”“应当”“可以”,不要把 SHOULD 当成可随意忽略的备注。原文也明确标注某些部分为非规范性说明;用例和示例帮助理解,实现验证规则要回到规范性算法。

网站开发者最值得反复对照的是§7 WebAuthn Relying Party Operations,而不是照抄 §1 的演示代码。原文给出的§1.1 阅读路线同样强调了这一点。

学习检查#

  • create() 成功后,为什么服务器还必须验证响应?
  • 服务器拥有公钥之后,为什么仍需每次生成不同的 challenge?
  • 为什么“给已有账户增加凭据”通常比从零设计无密码开户更适合作为第一步?

带着这三个问题进入下一章。它们对应的是信任边界、重放防护和账户绑定,也是最容易被“几行前端代码”掩盖的核心问题。

CHAPTER 02

术语与安全边界

学习 WebAuthn,先把“网站、浏览器、认证器、账户、凭据”区分开。它们承担不同职责;错误地把某一方返回的数据直接当成可信事实,往往比加密算法选错更容易造成漏洞。

英文原文 §4 Terminology · §6 WebAuthn Authenticator Model

参与方:谁信任谁#

英文术语 本书译法 具体职责
Relying Party / RP 依赖方,通常就是你的网站 产生请求,保存公钥,验证响应,管理账户和会话
WebAuthn Client / User Agent WebAuthn 客户端 / 用户代理 通常由浏览器与系统协作;检查调用来源、协调认证器、组织客户端数据
Authenticator 认证器 创建或使用凭据密钥,执行用户在场 / 验证操作,产生响应
User 用户 选择账户、授权创建或使用凭据
Credential 凭据 面向某个 RP 的认证材料;不能简单等同于一个用户账户
Ceremony 交互流程 由多方共同完成的注册或认证过程

网页脚本不能读取凭据私钥。网站收到的是凭据标识、公钥相关材料、签名及上下文数据。生物识别通常在认证器本地执行;网站只获得 UP/UV 等结果标志,不获得指纹图像、人脸模型或设备 PIN。英文原文 §14.3 本地生物识别

“不把私钥发给网站”并不等于“私钥永远只存在于一块硬件里”。现代同步型通行密钥可能由提供者在受保护的同步体系中同步;设备绑定型凭据有不同的保护与恢复模型。网站需要依据实际政策和验证结果判断保证程度,不能从一个指纹弹窗推断密钥一定不可导出。

origin 与 RP ID:一个精确来源,一个凭据作用域#

origin 是来源,通常包含协议、主机与端口;例如 https://login.example.com:8443。它不包含路径。

RP ID 是凭据的作用域标识,采用域名形式,没有协议、端口或路径。对于 Level 2 的常规 Web 场景,RP ID 必须是当前有效域名本身,或该域名的可注册域后缀;不能随意指定别人的域名,也不能使用公共后缀。英文原文:RP ID 定义

页面 origin 请求的 RP ID 是否符合常规域名关系
https://login.example.com:8443 login.example.com 是,精确主机
https://login.example.com:8443 example.com 是,可注册父域
https://login.example.com:8443 m.login.example.com 否,是子域而非父域
https://login.example.com:8443 com 否,是公共后缀
https://evil.example.net example.com 否,域名不匹配

浏览器的 RP ID 检查和服务器的 origin 检查解决不同问题。即便 login.example.com 和 admin.example.com 都能在域名规则上使用 example.com,服务器仍必须按本系统允许的 origin 精确验证。不要用字符串包含、模糊后缀匹配或接受所有 *.example.com 来代替明确的来源策略。

**域名选择是长期设计决策。**以 example.com 注册的凭据,不会因为你把新网站放到 example.net 就自动迁移。测试站点、Pages 预览 URL、生产域名应有明确边界;正式接入前应先确定稳定的生产 RP ID。英文原文 §7.1 注册验证

三种 ID,不要混用#

字段 谁生成 用途 常见错误
user.id / user handle RP 服务器 用不透明字节稳定标识本网站账户 使用邮箱、昵称或每次都变化的随机值
credential ID / rawId 认证器 找到某个具体凭据及其公钥 把它当成秘密,或当成整个账户的唯一凭据
challenge RP 服务器 绑定并区分本次操作,防止重放 写成固定值、由客户端决定、使用后不失效

用户句柄在创建时必须是 1~64 字节;建议给每个账户分配稳定的随机值,不包含用户名或邮箱。一个账户的多个凭据通常使用同一个 user handle。user.name 和 user.displayName 是展示字段,可改变,也可能重复;认证与授权不能依据显示名称决定。英文原文 §5.4.3 用户参数

credential ID 是不透明字节。网站应原样保存,不解析内部格式,不根据长度猜设备厂商。一个账户允许多个 credential ID,例如电脑内置认证器和备用安全密钥;一个 credential ID 对应的账户关系应由服务器保存并强制校验。

challenge:公开,但必须不可预测且只用一次#

challenge 不是密码,不需要向客户端保密;它的价值在于攻击者无法事先预测并重复使用。服务器使用密码学安全随机数生成器产生,记录预期值、操作类型、关联账户或会话和过期时间;完成验证时比对并消费。

原规范建议挑战至少 16 字节;本书示例采用 32 字节随机值。长度只是基本条件,Math.random() 拼成的字符串、时间戳和递增序号都不能替代安全随机数。timeout 只影响客户端交互,不会替服务器使挑战失效。英文原文 §13.4.3 Cryptographic Challenges

js
// 服务器思路示意;不是可接受客户端自报 challenge 的接口。
const bytes = crypto.getRandomValues(new Uint8Array(32));
// 将 bytes 对应的挑战与操作、会话、截止时间保存在服务器。
// 验证响应时读取服务器记录,而非从提交内容中建立“预期值”。

UP 与 UV:在场和验证不是同一个保证#

标志 含义 典型操作 网站能否据此推断身份细节
UP:User Presence 检测到用户在场并参与 触碰安全密钥按钮等 不能证明触碰者是哪位用户
UV:User Verification 认证器执行了本地用户验证 设备 PIN、指纹、面容等 不能由此知道使用了哪种具体生物特征

userVerification: "required" 表示本次操作必须执行 UV,服务器也必须检查响应的 UV 位。"preferred" 表示希望执行,但无法执行时不一定失败;"discouraged" 表示不要求 UV,不是强制禁止认证器执行。不要把 preferred 当成“总是要求指纹”。英文原文 §5.8.6 UV 需求

要求无密码、单步骤登录时,本书示例选择 required 作为学习策略。更广的兼容性策略需要结合风险、是否还有其他因素及用户群体确定。无论选什么值,服务器必须按自己的预期验证,不能信任客户端声称“已经验证”。

认证器类型与凭据类型是两条轴#

平台认证器(platform)通常集成在当前设备中;漫游认证器(roaming / cross-platform)可以用于不同设备,例如外接安全密钥。这描述的是认证器与客户端的附着关系。

可发现凭据(discoverable credential)允许客户端在服务器未提前列出凭据 ID 时找到可用于该 RP 的账户;不可发现凭据需要服务器在 allowCredentials 中提供 ID。旧名称 resident / non-resident 仍留在字段中,如 residentKey。这描述的是凭据发现能力。英文原文 §6.2 认证器分类

因此,“平台 = 可发现”“安全密钥 = 不可发现”“可发现 = 已同步到云端”都不是成立的等式。现代通行密钥学习中应分别检查:能否发现、是否执行 UV、是否允许备份、是否已经备份、认证器采用何种保护方式。

attestation 与 assertion#

attestation 译为“证明”,主要用于注册,可能为认证器来源或属性提供可验证信息。assertion 译为“认证断言”,主要用于登录,证明持有该凭据私钥的一方为本次挑战进行了操作。

普通消费类应用通常不需要识别硬件厂商,可采用 attestation: "none" 作为起点。这不代表跳过注册响应验证,也不代表公钥不需要解析;服务器仍然要验证挑战、来源、RP ID、标志及凭据结构。需要企业设备合规或特定硬件保证时,再设计证明信任策略和元数据维护。英文原文 §6.5 Attestation

学习检查#

  1. 指纹验证成功,网站究竟获得了什么信息?
  2. user.name 从“小林”改成邮箱后,为什么不应该影响已有凭据与账户的绑定?
  3. RP ID 正确时,为什么仍必须验证完整 origin?
  4. allowCredentials 为空,会允许攻击者不提供签名直接登录吗?

最后一题的答案是否定的。它改变的是“如何选择凭据”,服务器仍然必须找到已登记的公钥并完成全部认证验证;下一章会把这条信任链串起来。

CHAPTER 03

注册与认证:把信任链串起来

WebAuthn 的浏览器调用各只有一个主要入口,但一次安全登录需要服务器、客户端、认证器和用户共同完成。理解每一步携带什么、验证什么,才能判断代码是否真正实现了认证。

英文原文 §1.2 用例 · §1.3 API 示例 · §7 RP 操作

原文重点:你链接的登录场景#

你提供的锚点是 §1.2.2 Authentication。下面是该2021 年用例的中文译解,用来理解电脑与手机协作的角色,不应照搬为现代用户操作说明:

  1. 用户在电脑上通过蓝牙与手机配对,打开 example.com 并发起登录;浏览器提示到手机完成操作。
  2. 手机显示登录该网站的通知;用户打开通知,从账户列表中选择身份,再通过已配置的 PIN、生物识别等方式授权。
  3. 电脑上的网站显示所选账户已登录,并进入登录后的页面。

**现代补充:**今天的跨设备通行密钥体验可能由浏览器显示二维码并通过 hybrid 等机制协调。开发者应交给系统界面引导,不应要求所有用户先手工完成传统蓝牙配对。设备、浏览器和凭据提供者能力不同,也可能直接使用已同步到当前设备的通行密钥。Level 3 消费者多设备用例

这段用例说明了一个重要事实:执行网页的设备与使用私钥的设备可以不同。对 RP 服务器来说,最终仍然要验证同一套带上下文的响应,不能因为界面看起来像“手机确认”就降低验证要求。

注册:从已有账户到一个新公钥#

text
用户           RP 服务器              浏览器 / 系统             认证器
 |  已确认账户     |                       |                       |
 |--------------->|                       |                       |
 |                | 生成并保存 challenge  |                       |
 |                |----注册选项---------->|                       |
 |                |                       | create({ publicKey }) |
 |                |                       |----请求创建---------->|
 |<--------------------------显示账户 / 请求本地授权---------------|
 |---------------------------同意并完成 UV----------------------->|
 |                |                       |<---凭据与证明数据------|
 |                |<---注册响应 JSON-------|                       |
 |                | 验证并保存公钥 / ID    |                       |
 |<----添加成功----|                       |                       |

注册选项包括 RP 标识、用户句柄、随机挑战、支持的算法和认证器选择要求。浏览器会检查安全上下文和 RP ID 关系,组织客户端数据;认证器创建凭据并返回认证器数据及相关证明材料。

服务器收到响应后不能直接保存客户端随便提交的“公钥字符串”。它要依据§7.1 注册算法验证客户端数据、认证器数据、证明及本地策略,然后将验证得到的凭据公钥绑定到服务器早已确定的账户。

对不请求设备证明的普通应用,attestation: "none" 允许避免复杂证明信任链;这不会免除来源、挑战、RP ID 或 UV 检查。注册是否具有特定硬件来源保证,与“新公钥已安全绑定到哪个账户”是相关但不同的判断。

认证:用已保存公钥验证新挑战#

text
用户           RP 服务器              浏览器 / 系统             认证器
 |  点击登录       |                       |                       |
 |--------------->|                       |                       |
 |                | 生成新的 challenge    |                       |
 |                |----认证选项---------->|                       |
 |                |                       | get({ publicKey })    |
 |                |                       |----请求断言---------->|
 |<--------------------------选择账户 / 完成本地验证---------------|
 |                |                       |<---签名及上下文--------|
 |                |<---认证响应 JSON-------|                       |
 |                | 查公钥并完成全部验证   |                       |
 |                | 消费 challenge / 建会话|                       |
 |<----登录成功----|                       |                       |

服务器可以先让用户输入用户名,再下发该账户的 allowCredentials;也可以省略列表,依靠可发现凭据让用户在系统界面选择账户。二者的差别是凭据发现和账户定位方式,不改变签名与上下文校验的必要性。英文原文 §7.2 认证算法

签名到底覆盖什么#

认证时,签名的核心输入为:

text
authenticatorData || SHA-256(clientDataJSON)

|| 表示字节拼接。clientDataJSON 是浏览器提供的原始 UTF-8 JSON 字节,其中包含操作类型、challenge、origin 等。authenticatorData 包含 RP ID 的哈希、UP/UV 标志、签名计数器,以及可能存在的扩展数据。英文原文 §6.1 Authenticator Data · §5.8.1 客户端数据

服务端必须对收到的原始 clientDataJSON 字节做哈希。先解析为对象、再 JSON.stringify() 会改变空白、属性顺序等,可能得到不同字节。解析用于检查字段,原始字节用于签名输入,两项工作都需要。

公钥验证通过,只能说明签名与该输入相符;如果服务器没检查其中的挑战或 origin,攻击者仍可能把不属于当前操作的有效签名提交过来。因此,“调用验签函数返回 true”不等于“完整认证通过”。

密码登录与 WebAuthn 的差别#

维度 常见密码登录 WebAuthn 登录
用户侧认证材料 需记忆或由密码管理器填写的共享秘密 由认证器管理的凭据私钥
服务器主要保存 加盐密码哈希及参数 凭据 ID、公钥、账户绑定与状态
每次请求的核心证明 用户提交密码,服务器检查 对新挑战与上下文的签名
假站风险 用户可能把密码交给假站 标准客户端限制凭据的 RP 作用域
账户恢复 常见邮件 / 短信 / 人工流程 仍需设计;可结合备用凭据与恢复机制
服务器验证难点 密码哈希、限速、泄露防护 挑战生命周期、来源 / RP 绑定、签名及标志验证

公钥被读取不等于私钥泄露,因此服务器凭据库泄露的后果与密码哈希库不同;但账户信息、凭据 ID 和元数据仍有隐私与攻击价值,不能因此放弃访问控制或审计。

首次开户、添加凭据、重新认证#

这三个业务流程都可能调用 create() 或 get(),但授权条件不同:

  • **首次开户:**先确定产品如何分配账户和验证联系方式;WebAuthn 不会自动证明用户拥有某个邮箱。
  • **添加凭据:**必须基于可信账户上下文,例如已有登录会话,并视风险要求近期重新认证;防止被盗会话轻易增加攻击者凭据。
  • **敏感操作重新认证:**使用新的 challenge,明确绑定当前账户和操作;成功后授予有限时间或一次性的操作权限。

原文的§1.2.3 新设备注册用例展示了先用已有安全密钥登录,再添加新设备平台认证器的思路。它也说明一个账户允许多凭据,对设备更换和恢复很有价值。

浏览器响应与服务器响应要分开#

浏览器弹出成功动画,仅表示本地凭据操作完成;服务器可能因 challenge 过期、origin 不符或账户绑定错误而拒绝。前端应等待验证接口确认后再显示“登录成功”。

同样,NotAllowedError 可能涵盖用户取消、超时或无合适凭据等多种情况。页面应提供“未完成本次操作,可重试或选择其他方式”的正常反馈,而不是断言用户没有通行密钥或密码错误。

学习检查#

尝试分别说出注册和认证时服务器“发什么、记什么、验什么、存什么”。如果回答里缺少服务器保存的预期 challenge,或者只提到公钥验签,那么信任链还没有闭合。接下来的两章将把请求与返回字段逐一对应到这条链上。

CHAPTER 04

注册详解:安全地绑定一把新公钥

注册的目标是:让认证器创建一个面向本网站的凭据,然后由服务器验证并绑定到正确账户。navigator.credentials.create() 只完成浏览器侧的凭据创建;服务器验证成功,才算业务上的添加完成。

英文原文 §5.4 创建选项 · §5.2.1 注册响应 · §7.1 服务端注册算法

开始之前:先确定注册权限#

对于已有账户,注册接口应从可信登录会话读取账户 ID。不要接收一个客户端可随意修改的 userId,然后给这个账户添加公钥。敏感账户还应要求近期重新认证,并为成功添加或删除凭据生成审计记录。

服务器先生成随机 challenge,连同当前账户、注册操作和过期时间暂存。请求返回的选项和稍后收到的响应必须属于同一次流程;验证时消费挑战,防止重复提交、并发竞态和跨账户绑定。

创建选项逐项理解#

字段 类型 / 例子 它控制什么 应注意什么
challenge 浏览器中为字节;JSON 中常用 Base64URL 绑定本次注册请求 由服务器安全随机生成并保存预期值
rp.id example.com 新凭据的 RP 作用域 不能带协议 / 端口 / 路径;应长期稳定
rp.name 示例学习站 Level 2 中的 RP 显示名称 展示字段,不能作为安全身份
user.id 1~64 字节 账户的稳定 user handle 不包含邮箱等个人身份信息
user.name xiaolin@example.com 供用户识别的账户名 不是认证授权依据;考虑展示隐私
user.displayName 小林 友好的显示名称 可以与别的账户重复
pubKeyCredParams [{type:"public-key",alg:-7}] RP 接受的公钥算法,按偏好排列 服务端验证器必须实际支持选中的算法
excludeCredentials 已绑定凭据的描述列表 避免重复创建已有凭据 ID 解码为字节;服务器仍需检查凭据唯一性
authenticatorSelection 选择条件对象 可发现性、UV、附着类型要求 需求过严可能排除用户可用的认证器
attestation 通常从 "none" 开始 是否请求认证器证明 不等于是否验证注册响应
timeout 毫秒,如 60000 客户端等待时间提示 浏览器可调整;服务端另设过期策略
extensions 如 {credProps:true} 请求可选扩展 可能被忽略,不能假定一定有输出

其中 -7 表示 ES256,-257 表示 RS256。这些是 COSE 算法标识,不是任意数字;不要把它们与证书签名算法或 HTTP 层 TLS 算法混为一谈。英文原文 §5.8.5 算法标识

为通行密钥选择创建策略#

本书学习示例采用以下策略,适合练习可发现凭据与本地用户验证:

js
// 这是服务器产生创建选项时采用的策略片段。
const registrationPolicy = {
  authenticatorSelection: {
    residentKey: "required",
    requireResidentKey: true,
    userVerification: "required",
  },
  attestation: "none",
  extensions: { credProps: true },
};

residentKey: "required" 要求创建可发现凭据;无法满足时应失败。requireResidentKey: true 用于兼容较旧实现,保持与 residentKey 的要求一致。userVerification: "required" 要求完成本地用户验证,后端也必须执行对应检查。英文原文 §5.4.4 选择条件

这里没有强制 authenticatorAttachment: "platform",以便允许浏览器呈现适合的认证器选择。设置 platform 会限制为平台认证器,并不能保证“同步到所有设备”;设置 cross-platform 也不等于“必须是 USB”。选择附着类型应服务于业务要求,不应仅为了让演示弹出某一种系统窗口。

residentKey: "preferred" 可以扩大兼容范围,但可能得到不可发现凭据。若采用它,产品必须准备先输入用户名等登录路径,不能保证所有注册结果都能进行无用户名登录。credProps.rk 输出可帮助了解创建结果,缺少输出也不能简单推断为 false。英文原文 §10.4 Credential Properties

JSON 与二进制:最常见的接入错误#

HTTP JSON 不直接表达 ArrayBuffer。通常约定将二进制字段编码为 Base64URL 字符串;浏览器收到后,把 challenge、user.id、excludeCredentials[].id 解码为字节,再调用 create()。

Base64URL 用 -、_ 代替普通 Base64 的 +、/,通常省略末尾的 =。它是编码方式,不是加密。不要用 TextEncoder.encode(base64urlString) 解码:那会得到“编码字符串本身”的 UTF-8 字节。

以下辅助函数针对本文的无填充 Base64URL 约定;真实项目可使用经过验证的库。服务器仍须限制输入大小、验证编码与结构,不应把浏览器侧校验当成安全防线。

js
function fromBase64url(value) {
  if (typeof value !== "string" ||
      !/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1) {
    throw new TypeError("无效的 Base64URL 字符串");
  }
  const base64 = value.replace(/-/g, "+").replace(/_/g, "/");
  const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
  return Uint8Array.from(atob(padded), c => c.charCodeAt(0));
}

function toBase64url(value) {
  const bytes = ArrayBuffer.isView(value)
    ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
    : new Uint8Array(value);
  let binary = "";
  for (const byte of bytes) binary += String.fromCharCode(byte);
  return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}

async function postJSON(path, body = {}) {
  const response = await fetch(path, {
    method: "POST",
    credentials: "same-origin",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify(body),
  });
  if (!response.ok) throw new Error("操作失败,请重试");
  return response.json();
}

原生 API 注册示例#

这段是浏览器端集成示例,假设后端已实现同源的两个接口:/api/registration/options 直接返回 JSON 格式创建选项;/api/registration/verify 验证并保存凭据,通过服务端会话绑定账户和挑战。可运行的后端与浏览器源码见完整示例。

js
async function registerPasskey() {
  if (!window.isSecureContext || !window.PublicKeyCredential) {
    throw new Error("当前环境不支持 WebAuthn 安全调用");
  }

  const options = await postJSON("/api/registration/options");
  const publicKey = {
    ...options,
    challenge: fromBase64url(options.challenge),
    user: { ...options.user, id: fromBase64url(options.user.id) },
    excludeCredentials: (options.excludeCredentials ?? []).map(item => ({
      ...item,
      id: fromBase64url(item.id),
    })),
  };

  const credential = await navigator.credentials.create({ publicKey });
  if (!(credential instanceof PublicKeyCredential)) {
    throw new Error("未创建公钥凭据");
  }

  const response = credential.response;
  await postJSON("/api/registration/verify", {
    id: credential.id,
    rawId: toBase64url(credential.rawId),
    type: credential.type,
    clientExtensionResults: credential.getClientExtensionResults(),
    response: {
      clientDataJSON: toBase64url(response.clientDataJSON),
      attestationObject: toBase64url(response.attestationObject),
      transports: response.getTransports?.() ?? [],
    },
  });

  // 到这里才可向用户显示“通行密钥添加成功”。
}

普通界面应从清楚的“添加通行密钥”按钮触发,并在操作期间管理按钮状态。失败后恢复操作入口,把取消和超时作为正常情况处理。不要在页面加载时不断弹出注册提示。

这个 postJSON 仅演示传输。服务器还需验证请求来源、会话和 CSRF 策略;设置 Content-Type 或依赖“按钮只能被本人点击”都不能替代服务端权限检查。若扩展包含额外二进制输出,通用序列化器还需要针对扩展处理;本文代码只使用输出为布尔值的 credProps。

服务器应保存什么#

在完成规范校验之后,通常至少保存:账户 ID、稳定 user handle、credential ID、公钥字节及算法、签名计数器,以及可用于后续交互的 transport 提示。允许用户给凭据设置“办公室电脑”等备注,但不要把备注当作已验证的设备型号。

现代同步型凭据的备份状态与计数器处理有额外注意事项,应使用支持相应规范的验证器。不要只复制 Level 2 中某个字段判断片段,就断言所有现代通行密钥必须有严格递增计数器。

删除凭据时,服务器可以停止接受该 credential ID;这不等于删除了用户设备或同步提供者中的凭据。产品应说明这种区别,并避免删除账户最后一个可用登录 / 恢复途径后让用户被锁在账户外。

排错与自测#

现象 优先检查
调用即报 TypeError user.id 长度、必填字段、Base64URL 是否解码为字节
SecurityError HTTPS、RP ID 是否允许、iframe / 权限策略
NotAllowedError 用户取消、超时、无法满足 residentKey / UV 等要求
浏览器成功、服务器失败 challenge 是否属于同一会话、origin / RP ID 配置、编码是否改变
相同账户重复注册行为不符预期 excludeCredentials、user handle 稳定性、认证器的凭据管理行为

最值得做的反向测试是:修改返回响应里的 challenge、换一个 origin、把注册结果提交给另一个账户会话。这些情况必须由服务器拒绝,而不仅仅是在正常路径上看到一次“添加成功”。

CHAPTER 05

认证详解:从签名到可信登录会话

认证使用 navigator.credentials.get() 请求已有凭据产生断言。真正决定登录成功的是服务器:它必须找到已登记公钥,验证本次响应和预期上下文,再签发或更新登录会话。

英文原文 §5.5 认证请求选项 · §5.2.2 认证响应 · §7.2 验证认证断言

先选登录路径#

路径 A:先确定用户名#

用户先输入用户名或邮箱。服务器查出对应账户已登记的 credential ID,在 allowCredentials 中发送给浏览器。浏览器请求认证器使用这些凭据之一。

它适合兼容不可发现凭据,也适合需要先定位租户或账户的系统。服务器必须验证返回的凭据确实属于预期账户,且符合本次下发的允许列表,不能只按“签名有效”就接受任意账户的凭据。

查询接口还要考虑账户枚举:未知用户名与已知用户名在响应、错误消息、耗时和限速策略上的差异,可能暴露账户是否存在。不要在公开错误里直接显示“此邮箱存在,但尚未注册通行密钥”。英文原文 §14.6.2 Username Enumeration

路径 B:先选择通行密钥#

不提前输入用户名,服务器省略 allowCredentials 或发送空列表。客户端依靠可发现凭据,为该 RP 提供账户选择。此路径要求用户具有可发现凭据;普通不可发现凭据不会因列表为空而自动变得可发现。

服务器收到响应后,利用已登记的 credential ID 定位凭据与账户;还要验证 userHandle 与该凭据绑定的账户一致。此类未预先识别用户的流程应要求非空的有效 user handle,并将其与数据库中的稳定用户句柄比对,不能信任客户端额外附带的显示名称或账户 ID。英文原文 §7.2 第 6 步的账户识别规则

问题 先输入用户名 先选择通行密钥
allowCredentials 当前账户的已登记凭据列表 省略或为空
是否支持不可发现凭据 支持 通常不适用
账户何时确定 发出断言请求前 验证断言时通过登记记录确定
核心额外检查 凭据属于预期账户和列表 credential ID 与 user handle 一致地映射到已登记账户
主要体验取舍 多一步输入,定位明确 输入更少,依赖可发现凭据与账户选择界面

两种路径都要验证 challenge、origin、RP ID、UP/UV 和签名;不输入用户名只是移除了一个账户定位步骤,不会移除安全检查。

请求选项#

字段 例子 注意事项
challenge 服务器新生成的随机字节 与本次认证会话绑定、短时有效且只能使用一次
rpId example.com 必须与凭据创建时 RP ID 一致;注意字段名与注册的 rp.id 不同
allowCredentials {type,id,transports} 列表 ID 在调用前从 Base64URL 解码;transport 是交互提示
userVerification "required" 服务端也必须检查 UV;preferred 无法提供必然 UV 的保证
timeout 60000 客户端提示,不能代替服务器过期时间
extensions 可选扩展输入 只请求理解且实际需要的扩展

allowCredentials[].transports 可来自注册时的 getTransports(),有助于客户端呈现合适选择。它不是服务器证明私钥在何处的可信证据,也不应被误用为授权条件。英文原文 §5.8.3 凭据描述

原生 API 登录示例#

以下代码复用注册章中的 fromBase64url()、toBase64url() 和 postJSON()。假设 /api/authentication/options 直接返回 JSON 请求选项,服务器通过会话或等效机制保存预期挑战;/api/authentication/verify 只有在完整验证通过后才建立会话。

js
async function signInWithPasskey() {
  if (!window.isSecureContext || !window.PublicKeyCredential) {
    throw new Error("当前环境不支持 WebAuthn 安全调用");
  }

  // 此处为可发现凭据登录;服务端不预先指定用户名。
  const options = await postJSON("/api/authentication/options");
  const publicKey = {
    ...options,
    challenge: fromBase64url(options.challenge),
  };
  if (options.allowCredentials !== undefined) {
    publicKey.allowCredentials = options.allowCredentials.map(item => ({
      ...item,
      id: fromBase64url(item.id),
    }));
  }

  const credential = await navigator.credentials.get({ publicKey });
  if (!(credential instanceof PublicKeyCredential)) {
    throw new Error("未获得公钥凭据响应");
  }

  const response = credential.response;
  const result = await postJSON("/api/authentication/verify", {
    id: credential.id,
    rawId: toBase64url(credential.rawId),
    type: credential.type,
    clientExtensionResults: credential.getClientExtensionResults(),
    response: {
      clientDataJSON: toBase64url(response.clientDataJSON),
      authenticatorData: toBase64url(response.authenticatorData),
      signature: toBase64url(response.signature),
      userHandle: response.userHandle === null
        ? null
        : toBase64url(response.userHandle),
    },
  });

  // 只有服务器确认通过后,才显示登录成功或跳转。
  return result;
}

userHandle 允许为 null,尤其在已预先识别账户的某些流程中。序列化时要保留 null,不要把它编码成字符串 "null"、空字符串或全零字节。服务端根据本次登录路径,按规范检查其是否必须存在以及是否匹配。

上面的接口名称与完整示例一致。认证之后的会话 Cookie 应由服务器设置,JavaScript 返回的 {verified:true} 本身不是受保护资源的访问凭证。

服务器验证清单:每项都有关联目的#

以下是对 §7.2 的学习性归纳,生产代码应使用成熟验证器完成数据解析和密码学校验,并在其外层实现正确的业务状态管理:

  1. 检查响应结构、类型、大小与编码,确认对应一个本服务器发起且尚未过期的认证流程。
  2. 若曾下发非空 allowCredentials,确认返回 ID 在列表中;从数据库找到凭据、公钥及其账户关系。
  3. 验证账户识别规则,包括预期账户归属和需要时的 userHandle 一致性。
  4. 解析 clientDataJSON,要求类型是 webauthn.get;challenge 与服务器保存值一致;origin 属于精确允许列表;根据部署策略处理跨来源上下文。
  5. 验证 authenticatorData.rpIdHash 等于预期 RP ID 的 SHA-256;检查 UP,以及策略要求时的 UV。
  6. 处理请求的扩展结果,使用该凭据登记的公钥和算法,验证 authenticatorData || SHA-256(clientDataJSON) 上的签名。
  7. 按计数器与现代凭据的备份能力策略处理风险及状态更新;原子消费挑战,完成必要记录,然后建立会话。

“原子消费”是为了防止两个并发请求使用同一挑战都通过。短有效期和服务端签名的 token 也不能自动保证一次性;若采用无状态挑战,需要另行解决重放窗口与消费状态。

关于签名计数器#

signCount 可辅助发现某些凭据克隆或状态异常,但不能简单当成“必须从 1 开始、每次严格加一”的业务计数。Level 2 已允许不实现计数的认证器使用零;当新旧值均为零时,不能因此认定克隆。

对于支持计数的情况,异常可作为风险信号;是否拒绝、要求补充验证或记录风险,需要结合认证器行为和业务策略。现代同步型通行密钥还应结合 Level 3 的备份语义,避免机械套用旧式硬件密钥假设。英文原文 §6.1.1 Signature Counter Considerations · Level 3 认证器数据

条件式界面:现代补充#

条件式 mediation 可让通行密钥出现在登录输入框的自动填充选择中。它属于本书的现代补充,不是你提供的 Level 2 示例流程。通常使用带 autocomplete="username webauthn" 的输入框、可发现凭据,以及 mediation: "conditional" 的请求,并先检测能力。

js
const canUseConditionalUI =
  window.PublicKeyCredential &&
  typeof PublicKeyCredential.isConditionalMediationAvailable === "function" &&
  await PublicKeyCredential.isConditionalMediationAvailable();

功能检测成功只表示当前环境报告支持此能力,不证明账户已有通行密钥,也不保证每一种认证器都支持。实际调用仍需服务器挑战、正确序列化和完整验证;保留普通按钮登录路径。英文原文 Level 3 条件式 mediation

取消、重试与错误提示#

页面切换登录方式或用户离开时,可用 AbortController 取消尚未完成的浏览器请求;不要同时挂起多个相互竞争的 get()。取消之后,服务器仍要按自己的过期 / 消费策略处理挑战。英文原文 §5.6 Abort Operations

把“取消 / 超时”“验证失败”“不支持”分别记录便于排错,但对外提示应避免暴露账户是否存在、公钥细节或内部验证错误。重试应获取新的挑战,不要将旧响应再次提交。

学习检查#

  • 把上一次成功响应原样重放,服务器是否拒绝?
  • 使用另一账户的有效凭据,是否会被错误登录到当前预期账户?
  • 修改 origin 或 RP ID,签名或上下文校验是否会失败?
  • 要求 UV 时,响应缺少 UV 位是否被拒绝?
  • 在验证接口成功之前,页面是否就已经显示“已登录”?

通过正常路径仅说明功能能够工作;这些反向检查才能说明关键安全约束确实在服务器生效。

CHAPTER 06

服务端验证:安全判断发生在哪里

浏览器弹出系统窗口、用户完成指纹验证,都不代表你的网站已经确认登录。最终决定来自服务器:使用已保存的公钥,验证本次响应,并确认它属于正确的操作与账号。

英文对照:§7.1 注册新凭据 · §7.2 验证认证断言。以下是按开发职责整理的中文释义与工程补充;不是替代原文的逐步规范算法。

把验证分成三层#

层次 谁负责 本地示例如何处理
请求上下文 应用 固定来源、会话 Cookie、操作类型、一次性 challenge 与有效期
WebAuthn 结构与密码学 验证库 解析客户端数据、CBOR / COSE、RP hash、标志、证明与签名
账号授权与状态 应用 凭据归属、user handle、唯一性、计数更新、成功后建立会话

成熟验证库能减少密码学实现错误,但无法知道你的业务账号属于谁。因此,“库返回成功”仍不能代替账号绑定检查。

注册响应需要核对什么#

按 §7.1 原文阅读时,重点追踪:

  1. 客户端数据的类型是 webauthn.create;challenge、origin 与服务端预期一致。
  2. 解码证明对象,验证 RP ID hash、UP;要求 UV 时必须检查 UV。
  3. 公钥算法属于发出的算法集合;扩展结果按使用策略处理。
  4. 按证明格式验证,并决定其信任结果是否符合 RP 策略。
  5. 检查凭据 ID 的账号归属与唯一性,再保存公钥等结果。

本示例明确接受 none 证明策略,因此不要求设备厂商证书。它仍校验注册数据;同时也不声称能证明设备型号、私钥处于硬件内或用户的现实身份。

登录响应需要核对什么#

§7.2 原文可按下面的顺序理解:

  1. 根据凭据 ID 查找已登记的公钥,确认凭据属于目标用户;有允许列表时还要检查列表约束。
  2. 无用户名登录需要有效的 userHandle,并确认它和凭据归属指向同一账号。
  3. 检查类型 webauthn.get、challenge、origin、RP ID hash、UP 与所要求的 UV;处理扩展。
  4. 验证 authenticatorData || SHA-256(clientDataJSON) 的签名,评估计数器。
  5. 只有全部通过后,更新状态并建立登录会话。

|| 在这里表示二进制拼接,不是 JavaScript 的逻辑或。clientDataJSON 要使用响应里的原始字节计算哈希,不能先解析 JSON 再重新序列化。

对照可运行代码#

配套服务端源文件使用以下调用进行登录验证;API 已按锁定版本 @simplewebauthn/server 14.0.3核对:

js
const result = await verifyAuthenticationResponse({
  response: req.body,
  expectedChallenge: pending.challenge,
  expectedOrigin: 'http://localhost:3000',
  expectedRPID: 'localhost',
  credential, // 从服务端存储取出的 id、publicKey、counter 等
  requireUserVerification: true,
});

if (!result.verified || !req.session.active) {
  throw new Error('认证验证未通过');
}
credential.counter = result.authenticationInfo.newCounter;

credential 不是客户端提交的公钥对象。攻击者若能同时指定“要验证的签名”和“拿来验签的公钥”,就可以用自己的钥匙证明自己提交的数据。

一次性 challenge 为什么要先消费#

示例在进入异步验证库之前,先同步取出并删除当前会话的待验证操作:

js
function consumeChallenge(session, kind) {
  const pending = session.pending;
  delete session.pending;
  if (!session.active || !pending || pending.kind !== kind ||
      pending.expires <= now()) {
    throw new Error('挑战缺失、已使用或已过期;请重新开始');
  }
  return pending;
}

这样同一响应不能反复尝试,失败也必须重新申请 challenge。服务端还把待验证数据绑定到不透明会话 ID,注册操作保存服务端生成的账号 ID;请求体里没有可任意指定账号的字段。

示例只支持单进程。共享数据库、多个 Worker 或多个容器不能依靠内存删除实现相同保证,需要数据库的原子消费,并为凭据状态更新处理并发。可继续看生产检查。

计数器是信号,不是绝对结论#

不能要求所有认证器每次都返回更大的数字。一直为 0 的计数器是允许出现的;非零计数回退也不能单独证明发生克隆。英文原文 §6.1.1

本示例沿用验证库策略:接受双方都为 0;若任一值非零而新值不大于已存值,验证库会拒绝。验证与更新之间用单进程锁避免交错。这个策略是示例的明确选择,真实系统应结合认证器类型、并发情况和风险政策设计处置方式。SimpleWebAuthn 官方说明

下一步:运行完整示例,再回来看每个检查分别在哪里执行。

CHAPTER 07

完整示例:本地注册与无用户名登录

下载并运行一套完整的 Node.js 示例,从发出 challenge、调用浏览器 API,到服务器验签和建立会话。它创建独立的匿名学习账号,无需邮箱或密码,也不会给输入的任意用户名绑定凭据。

下载完整示例 ZIP · 查看运行说明 · 服务端源代码 · 浏览器代码 · 自动测试

这是在电脑本地运行的实验。本站 Pages 服务提供文档与源码下载;线上文档页不运行认证服务,也不保存读者的通行密钥记录。

运行条件与命令#

需要 Node.js 22 或更高版本,以及支持 WebAuthn 的浏览器和能执行本地用户验证的认证器。实际是否出现指纹、面容、PIN、安全密钥或手机选项,取决于浏览器、系统和设备配置。

ZIP 解压后,进入含有 package.json 的 examples 目录;项目源码中的目录也叫 examples:

sh
cd examples
npm ci
npm test
npm start

浏览器打开 http://localhost:3000。示例固定校验这个 origin 和 localhost RP ID,因此不要替换为 127.0.0.1、局域网 IP 或别的端口。端口被占用时先停止占用服务。Node.js 版本下限依据 SimpleWebAuthn 官方文档;安装文件锁定 @simplewebauthn/server 14.0.3 和 Express 5.3.0,便于复现。

按这四步练习#

  1. 点击“创建实验账号与通行密钥”,完成系统提示。页面显示匿名账号和凭据元数据。
  2. 点击“退出当前账号”。这会替换会话,保留服务端内存中的凭据记录。
  3. 点击“用通行密钥登录”,选择刚才创建的凭据并完成验证。
  4. 对照登录前后的账号 ID;应回到同一个实验账号。可以在浏览器开发者工具的 Network 面板查看四个协议接口。

进程结束后,账号、公钥和 challenge 全部清空;设备上的通行密钥可能仍在。重启服务后旧凭据会找不到对应账号,可在设备的密码 / 通行密钥管理器中删除该实验记录,再重新注册。

文件职责#

文件 阅读重点
server.js challenge 生命周期、服务端验证、账号归属、会话与计数更新
public/app.js 按钮交互、两个原生 WebAuthn API、发送 JSON 响应
public/browser.js JSON 中 base64url 与 WebAuthn 二进制字段转换
public/index.html / style.css 本地实验页面,无额外构建步骤
server.test.js 软件生成 ES256 密钥,测试真实验签与拒绝路径
package-lock.json 固定本次可复现的依赖树

四个接口如何串起来#

接口 服务器做什么 浏览器接着做什么
POST /api/registration/options 分配随机账号 ID,生成并保存注册 challenge 转换二进制字段,调用 create()
POST /api/registration/verify 消费 challenge,验证后保存账号与凭据 显示注册成功
POST /api/authentication/options 生成并保存登录 challenge,不限制凭据列表 调用 get(),让用户选择账号
POST /api/authentication/verify 验证归属与签名,更新计数并轮换会话 显示已登录账号

所有 POST 都要求本地 origin 和 JSON 内容类型。HttpOnly、SameSite=Strict 的 Cookie 保存不透明随机会话 ID;challenge 保留在服务器中。注册时每个浏览器会话只拥有自己的待创建账号,无法指定已有账号来添加公钥。

注册代码讲解#

服务端创建注册选项:

js
const options = await generateRegistrationOptions({
  rpName: 'Passkey 中文学习实验',
  rpID: 'localhost',
  userID: Buffer.from(userId, 'base64url'),
  userName: `learner-${userId.slice(0, 8)}`,
  challenge: randomBytes(32),
  attestationType: 'none',
  authenticatorSelection: {
    residentKey: 'required',
    userVerification: 'required',
  },
  supportedAlgorithmIDs: [-7, -257],
});

userId 是服务器产生的 32 字节随机标识。residentKey: 'required' 使示例能够采用可发现凭据的无用户名登录;userVerification: 'required' 同时在服务端验证中强制执行。库会生成适合 JSON 的结构,并补充 requireResidentKey 与 credProps 等选项;参数解释见注册详解。

浏览器把选项转换后,创建凭据并提交结果:

js
const options = await request('registration/options');
const credential = await navigator.credentials.create({
  publicKey: registrationOptions(options),
});
if (!credential) throw new Error('没有创建凭据');
return request('registration/verify', credentialJSON(credential));

request() 是示例 app.js 中的同源 POST 辅助函数;registrationOptions() 和 credentialJSON() 来自 browser.js。上面的片段来自完整程序,需要在其上下文中阅读。

无用户名登录如何确认账号#

服务端发出的请求没有 allowCredentials 限制。用户选中凭据后,服务器先根据凭据 ID 查记录,再检查 response.userHandle 是否与记录中的随机账号 ID 相等,最后使用已保存的公钥验证签名:

js
const credential = credentials.get(req.body.id);
if (!credential || req.body.response?.userHandle !== credential.userId) {
  throw new Error('凭据与账号不匹配');
}

查到记录不代表已登录。此处之后仍要验证 challenge、origin、RP ID、UP / UV、签名与计数器,成功后才轮换会话。已登录用户点注册按钮会被阻止;想创建另一个独立实验账号时先退出。

测试验证了什么#

npm test 使用 Node.js 自带测试运行器,无需额外测试框架。测试生成软件认证器响应,由真正的验证库解析 CBOR、处理 COSE 公钥并验证 ES256 签名;覆盖以下行为:

  • 有效注册、退出、登录,以及成功后拒绝旧响应重放。
  • 失败后 challenge 失效、操作类型约束、跨会话不可使用、五分钟过期。
  • 篡改 challenge、origin、RP ID、UV 标志、签名或 user handle 均不能登录。
  • 非零计数回退被拒绝,始终为零的计数器可以多次登录。
  • Host / HTTP Origin 限制、请求频率上限,以及浏览器二进制转换。

软件响应测试能验证后端路径,不代替真实设备、系统 UI、同步提供方与跨设备流程测试。人工练习时仍应完成上面的四步;上线前的验证范围见生产检查与排错指南。

实验边界#

示例只监听本机,最多存放 500 个凭据、1,000 个会话;同一 IP 每分钟最多 120 个 API 请求。会话一小时过期,challenge 五分钟过期;客户端 timeout: 60000 是另一项提示。

它没有持久化、多实例事务、真实身份验证、恢复、添加第二个凭据、删除凭据或线上 HTTPS 会话配置。将它移到服务器时,不能仅把 localhost 替换成域名就视为生产接入。继续阅读生产系统设计。

CHAPTER 08

现代 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 接口

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

CHAPTER 09

从学习示例到生产系统

Passkey 解决凭据证明问题;账号归属、会话、恢复和凭据管理,仍然是应用自己的责任。本章把规范中的安全与隐私要求整理成开发检查项。

英文原文:§13.4 RP 的安全考量 · §14.6 RP 的隐私考量 · §15 无障碍考量。以下架构与 SQL 为学习补充。

首先保护“添加凭据”这一步#

攻击者不一定要伪造签名。如果他能给受害者的账号绑定自己的 Passkey,以后就能正常签名登录。因此,添加凭据必须绑定到服务端已经确认的账号身份。

给现有账号添加凭据时,从经过认证的会话取账号 ID,视风险要求近期重新认证;不要直接相信请求体里的 userId、邮箱或用户名。新用户注册则先完成你的账号创建和身份归属流程,再把新凭据关联进去。验证码认证只能证明该次验证对应的控制权,不能自动替代所有高风险操作的身份要求。

配套示例采用独立的随机演示账号,让你练习协议;它没有实现邮箱身份验证,也不允许靠输入已有用户名给别人添加凭据。

Challenge 的完整生命周期#

§13.4.3 的中文释义:注册和认证的 challenge 必须在 RP 信任的环境中随机生成;返回值必须与发出的值匹配。不要依靠客户端维护这项约束。规范建议至少 16 字节,以提供足够的不可预测性。

实际开发可以生成 32 个密码学随机字节,并为每次操作保存以下上下文:

字段 作用
challenge 关联这次响应与服务端发起的操作
sessionId 或不透明事务标识 防止跨会话挪用
ceremony 区分注册和登录,不能相互替换
accountId 注册绑定当前账号;已知账号登录则绑定目标账号
expiresAt 限制有效时间;页面上的 timeout 不是服务端过期校验
固定的 origin、RP ID、UV 策略 验证时使用可信配置,不能从响应反推

应以原子操作消费 challenge,避免两个请求同时通过。下面是 PostgreSQL 的设计示意,不是配套示例的运行依赖:

sql
DELETE FROM webauthn_challenges
WHERE transaction_id = $1
  AND session_id = $2
  AND ceremony = 'authentication'
  AND expires_at > now()
RETURNING challenge, account_id;

取不到记录就拒绝;取到以后,无论本次验证成功或失败,都不能再次使用。失败的用户重新申请一个 challenge。登录成功后的凭据计数更新也要处理并发,不能让旧响应覆盖新状态。

不要把 challenge 当作第二份密码:它可以经过浏览器,但必须不可预测、有上下文、限时且只能用一次。对签名验证、安全标志和 origin 的检查仍然必不可少。

数据库实际保存什么#

一个账号通常对应多条凭据。以下为关系模型示意:

sql
CREATE TABLE passkey_credentials (
  credential_id TEXT PRIMARY KEY,
  account_id UUID NOT NULL REFERENCES accounts(id),
  public_key BYTEA NOT NULL,
  sign_count BIGINT NOT NULL DEFAULT 0,
  transports JSONB NOT NULL DEFAULT '[]',
  device_type TEXT,
  backed_up BOOLEAN,
  label TEXT,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
  last_used_at TIMESTAMPTZ
);
CREATE INDEX passkey_account_idx ON passkey_credentials(account_id);

credential_id 用统一的 base64url 文本编码,公钥保存验证库给出的字节。还应按所用库保存它要求的算法、备份状态等元数据。transports 是交互提示,不是可信的身份断言。label 是用户起的名字,也不能据此判断设备可信度。

账号表另存稳定、随机且无个人信息的 user_handle,长度不得超过 64 字节。一个账号的多个凭据使用同一个 user handle。不要把邮箱、用户名或可猜测的无盐哈希塞进 user.id。§14.6.1 原文

服务器不保存 Passkey 私钥,也不需要收到用户的指纹、面容或设备 PIN。现代凭据提供方可能在其自己的安全体系中同步私钥;这不等于你的 RP 服务端获得私钥。

RP ID 是长期的身份边界#

RP ID 不带协议、端口或路径;origin 则包括协议、主机以及非默认端口。例如 https://login.example.com:8443 是 origin,example.com 可以是符合域名规则的 RP ID。服务端必须分别验证它们。RP ID 定义

把生产、预发布、本地环境分开配置。不要使用 endsWith('example.com') 自制 origin 白名单,因为 evil-example.com 也满足这个字符串条件。使用完整、明确的 origin 列表,并让验证库完成协议校验。

已有凭据与注册时的 RP ID 绑定。域名迁移前必须设计过渡和重新注册流程,不能修改数据库中的一个字符串就指望旧凭据跟随迁移。Cloudflare Pages 的预览地址也可能与正式地址不同;若以后将示例做成真正线上应用,需要以稳定域名规划 RP ID。本文发布的 Pages 站点仅承载文档与源码下载。

会话仍然需要保护#

验证成功后,由服务器签发或轮换应用会话。生产 Cookie 通常使用 Secure、HttpOnly、适当的 SameSite、明确的有效期和受控作用域。退出时销毁服务端会话,敏感操作校验 CSRF 或来源,并限制尝试频率。

Passkey 抗钓鱼能力依赖正确的域绑定和验证。它不会自动修复你站点的 XSS、被盗会话、越权 API 或错误的账号恢复流程。浏览器调用成功只代表拿到了响应,不代表用户已经通过你的服务端认证。

计数器是风险信号#

L2 描述了签名计数器,用于发现可能的凭据克隆或设备异常;它也允许不支持计数的认证器始终返回零。现代同步 Passkey 进一步要求考虑多设备场景。不能把所有凭据的计数都假定为严格递增。

保存验证库返回的计数和备份信息;结合凭据类型、并发、供应商行为和业务风险制定异常策略。不要为了兼容而关闭签名、challenge 或 origin 校验。本文示例沿用验证库的计数检查,遇到异常拒绝并重新发起;生产系统需明确自己的风险处置策略。L2 §6.1.1 · L3 §6.1.1

恢复流程决定整体安全下限#

§13.4.6 的中文释义:L2 本身没有定义私钥备份或在认证器之间共享私钥的协议;RP 应允许并鼓励用户为同一账号注册多个凭据。这是 2021 年规范的语境,不能据此推断今天所有 Passkey 都不可同步。

生产系统应支持多个凭据、可识别的名称、最近使用时间、逐个撤销,以及丢失全部凭据后的恢复。删除最后一个凭据时,先确认还有可用的恢复或登录途径。删除服务器记录会阻止该凭据登录本站,但通常不会自动删除设备或密码管理器里的副本。

恢复流程要与账号风险相称。一个容易被接管的邮箱恢复入口,仍可能绕过很强的 Passkey 登录。恢复成功、添加或删除凭据时,应记录审计事件并按产品政策通知用户。

隐私、可用性与验证#

用户名存在性、allowCredentials 的数量和长度都可能泄露账号信息。对不存在账号与验证失败给出一致的外部错误;需要时采用无用户名的可发现凭据登录。把精确原因留给受控的服务端日志,不记录原始会话令牌、完整响应或生物信息。§14.6.2 · §14.6.3

上线前至少验证这些行为:

  • 正常注册、退出、重新登录;一个账号绑定第二个凭据。
  • challenge 过期、重放、跨会话使用,以及注册响应用于登录。
  • 错误 origin、RP ID、签名、用户验证标志和凭据归属都被拒绝。
  • 同一响应并发提交只成功一次;旧计数更新不会覆盖新数据。
  • 用户取消、凭据不可用、没有生物识别时有清楚的提示或替代路径。
  • 手机、桌面、外接安全密钥的真实操作,以及键盘和屏幕阅读器使用。

规范建议认证器提供多种用户验证方式,并给用户足够时间完成操作。不要把“人脸或指纹”写成唯一途径,也不要用过短的超时排除行动不便的用户。

学习自测:如果有人成功签名,但返回的是另一个账号的凭据,你的服务端会在哪一步拒绝?如果只能回答“前端不会这么做”,账号归属校验还没有完成。

CHAPTER 10

调试、错误与测试

先确定失败发生在哪一层:选项生成、浏览器调用、JSON 编解码,还是服务端验证。把所有问题归结为“指纹失败”,会掩盖最常见的配置和协议错误。

英文原文:§5 Web Authentication API · §5.6 中止操作 · §11 用户代理自动化。下面的排查表和测试方法是开发补充。

一张表定位常见错误#

现象 优先检查 正确处理
PublicKeyCredential 不存在 浏览器、上下文和能力检测 提示当前环境不可用,保留可用登录方式
SecurityError 是否是安全上下文;RP ID 是否符合当前域规则 分别检查 location.origin 与服务端固定 RP ID
NotAllowedError 用户取消、超时、无可用凭据或策略限制等 提供重试入口;不要断言“用户取消”是唯一原因
InvalidStateError(注册) excludeCredentials 是否匹配已存在的凭据 告知用户可能已添加过,允许换用另一个认证器
NotSupportedError 算法列表、认证器能力、选项组合 使用受支持算法和适合产品的认证器要求
TypeError challenge、user.id、凭据 ID 的类型和长度 JSON 的 base64url 必须在调用原生 API 前转成字节
页面成功弹窗,服务端拒绝 challenge、origin、RP ID、UV、签名和账号映射 按验证步骤定位;不能因为弹窗成功而跳过验证
刷新、并发点击后失败 challenge 被覆盖、消费或过期 禁用重复提交,为新尝试申请新 challenge
同一域名换端口后失败 expected origin 是否包含正确端口 为环境设置完整 origin;RP ID 不包含端口
开发能用,线上不能用 HTTPS、正式域名、代理和 Cookie 独立配置环境,查看真实响应状态与 Cookie

异常类型可能随平台和具体失败阶段变化;以捕获的错误、请求状态和规范算法共同判断,不能用这张表推断唯一根因。

浏览器诊断片段#

js
const available = window.isSecureContext && 'PublicKeyCredential' in window;
console.table({
  origin: location.origin,
  secureContext: window.isSecureContext,
  webauthnAPI: available,
});

if (available) {
  const platformUV = await PublicKeyCredential
    .isUserVerifyingPlatformAuthenticatorAvailable();
  console.log('是否有支持用户验证的平台认证器:', platformUV);
}

platformUV === false 不代表所有 WebAuthn 登录都不可用,用户还可能使用外接认证器或其他平台提供的途径。也不能由这个布尔值推断用户是否注册过本站凭据。能力检测用于改善界面,不能作为服务端认证依据。§5.1.7 原文

生产环境使用 HTTPS。http://localhost 是浏览器认可的本地开发场景;手机访问电脑的 http://192.168.x.x 并不等价。设备之间联调时,需要可信 HTTPS,并同步调整固定的 origin 与 RP ID。

取消正在等待的调用#

用户切换账号、离开页面或重新发起认证时,可以用 AbortController 结束旧请求:

js
const controller = new AbortController();

document.querySelector('#cancel').addEventListener('click', () => {
  controller.abort();
});

try {
  // publicKey 来自服务端,并已把二进制字段解码为 Uint8Array
  const assertion = await navigator.credentials.get({
    publicKey,
    signal: controller.signal,
  });
  if (!assertion) throw new Error('未返回凭据');
  // 接下来序列化并提交服务端验证
} catch (error) {
  if (controller.signal.aborted) {
    console.log('本次等待已取消,可以重新开始');
  } else {
    console.error(error.name, error.message);
  }
}

这是 API 用法片段,要求页面中已有 #cancel 按钮和经过解码的 publicKey。不要复用已经中止的 controller。取消浏览器等待不等于服务端自动销毁 challenge,服务端仍然需要过期和一次性消费策略。

看懂数据,不改写签名输入#

clientDataJSON 是原始 UTF-8 JSON 字节;可解码查看 type、challenge、origin 和 crossOrigin。登录的 type 应为 webauthn.get,注册为 webauthn.create。

js
// browserResponse 是 navigator.credentials.get() 返回的凭据
const text = new TextDecoder().decode(browserResponse.response.clientDataJSON);
const clientData = JSON.parse(text);
console.log(clientData.type, clientData.origin);

本地调试可读字段,验证时却必须以收到的原始字节计算哈希。先解析再 JSON.stringify() 会改变空格、键顺序或转义方式,使签名输入改变。也不要把二进制数据错误地当成十六进制字符串或普通 Base64。

对于完整响应,优先观察字段是否存在、解码后字节数、请求关联 ID 和验证阶段;生产日志不应成为可重放响应或账号标识的集中仓库。§5.8.1 原文

把测试分成三层#

层次 验证什么 无法证明什么
服务端单元与接口测试 过期、重放、会话绑定、错误参数、库的验证拒绝 真实浏览器弹窗和设备行为
虚拟认证器端到端测试 从注册、存储到登录的真实签名与 API 交互 生物识别、蓝牙、硬件和设备同步体验
真实设备测试 平台交互、取消、恢复、跨设备可用性 所有未来系统或浏览器版本都一致

L2 §11 定义虚拟认证器自动化模型。浏览器开发工具和自动化工具可以据此模拟认证器;这适合持续集成,但应另做真实设备验证。自动化中把“用户验证成功”设为真,也只是在模拟 UV,不能声称测试过真实指纹。

配套源码含可运行测试;源码 README 记录验证范围。将它作为你的测试起点,然后补充账号绑定、恢复、撤销和部署域名相关的应用测试。

遇到失败时的记录模板#

text
环境:浏览器 / 操作系统 / 认证器类型
操作:注册 / 有用户名登录 / 无用户名登录
配置:固定 RP ID、预期 origin(不含令牌)
阶段:options / create|get / serialize / verify
外部表现:异常 name、HTTP 状态、请求关联 ID
服务端原因:challenge expired / origin mismatch / ...
复现:是否稳定,换设备、重试或刷新后如何变化

一次只改变一个变量。先修复域名与编解码错误,再判断认证器兼容性;不要为排查问题临时关掉验证并把配置带到生产。

CHAPTER 11

英文规范导读与章节对照

掌握注册与登录后,回到英文规范核对边界。这份导读保留原有章节与链接,帮助你按问题定位,而不把浏览器和认证器实现算法误当作必须自己手写的业务代码。

主来源:Web Authentication: An API for accessing Public Key Credentials — Level 2,W3C Recommendation,2021 年 4 月 8 日。以下是章节释义与索引;完整定义、算法顺序和要求以英文原文为准。

规范用词如何理解#

§2 Conformance 说明一致性要求。阅读规范时,特别注意这些词:

英文 中文理解 开发时的含义
MUST / MUST NOT 必须 / 不得 一致性要求,不能为了让示例通过而忽略
SHOULD / SHOULD NOT 应当 / 不应当 通常遵循;例外需要理解后果和适用条件
MAY 可以 可选能力,不能假定所有实现都有
non-normative 非规范性 示例、背景和解释,不是独立的合规算法

同一章节可能同时包含规范性算法、说明和 Note。本文的“建议”若标为开发补充,不应被误引为 W3C 的 MUST。

完整章节导航#

原文 中文主题 对本站开发者最直接的用途
§1 Introduction 简介、用例和 API 示例 理解两种仪式与参与者
§2 Conformance 一致性要求 区分 RP、浏览器、认证器各自的要求
§3 Dependencies 外部依赖 找到 Web IDL、HTML、Credential Management 等基础规范
§4 Terminology 术语 澄清凭据、认证器、用户句柄和 RP ID
§5 Web Authentication API 浏览器 API 查选项、响应类型和错误条件
§6 Authenticator Model 认证器模型 理解认证器数据、公钥、签名和计数器
§7 Relying Party Operations RP 操作 注册与认证验证的核心依据
§8 Attestation Statement Formats 证明语句格式 有设备来源或企业准入需求时深入
§9 WebAuthn Extensions 扩展机制 理解客户端与认证器扩展如何处理
§10 Defined Extensions 已定义扩展 查 credProps、largeBlob、AppID 等
§11 User Agent Automation 自动化 建立虚拟认证器测试
§12 IANA Considerations 标识符注册 实现底层格式或扩展时参考
§13 Security Considerations 安全考量 challenge、凭据丢失、证明的局限
§14 Privacy Considerations 隐私考量 用户句柄、账号枚举和跨站关联
§15 Accessibility 无障碍 足够的时间、多种验证方式与提示
§16 Acknowledgements 致谢 原规范贡献者
Index · References · IDL Index 索引、参考文献和接口 精确查定义和类型

本文完整保留了规范的阅读入口,但没有逐字翻译所有认证器内部算法、证明格式和 Web IDL。学习与实现本站示例所需的内容,在前面章节按流程展开。

§6:认证器数据的结构#

§6.1 Authenticator Data 定义一段字节序列,而不是 JSON 对象。其开头包含:

字节内容 长度 意义
rpIdHash 32 字节 RP ID 的 SHA-256 哈希
flags 1 字节 用户在场、用户验证及后续数据等标志
signCount 4 字节 大端无符号签名计数
attested credential data 可变,可选 注册公钥、凭据 ID 和 AAGUID 等
extensions 可变,可选 扩展输出

应交给成熟验证库按格式和标志解析。不能简单用“长度大于 37 字节”证明有效。现代 L3 又定义了备份相关标志;不要把 L2 未定义位的解释套在所有新凭据上。

认证断言的核心签名输入为:

text
authenticatorData || SHA-256(clientDataJSON)

这里的 || 表示拼接原始字节。clientDataJSON 中包含 challenge 与 origin,authenticatorData 中包含 RP ID 哈希和用户操作标志。服务端必须既验证签名,也校验这些字段符合自己发出的请求。

§6.5 与 §8:Attestation 证明什么#

Authentication assertion 用已注册的凭据私钥证明本次认证;Attestation 在注册阶段表达新公钥的来源与认证器属性。后者可能涉及设备厂商、证书链和信任策略,并不证明注册者就是某个业务账号的合法主人。

常规公众站点通常可以从 attestation: 'none' 开始,以减少隐私和部署负担。这不等于“跳过注册验证”:仍然需要解析 attestation object,提取公钥,验证 challenge、origin、RP ID、算法、标志和其他必需条件。该选择也不能作为“私钥必定存放在某种硬件”的证据。

格式入口 中文解释
packed 通用紧凑证明格式
TPM 与可信平台模块结构相关的证明
Android Key Android 密钥证明
Android SafetyNet L2 中保留的历史格式;不能据此推断服务当前可用性
FIDO U2F U2F 兼容证明
none 不提供可验证设备来源的证明语句
Apple anonymous Apple 匿名证明格式

企业若必须限制认证器型号,需要额外的证明校验、可信根与元数据、撤销处理和隐私评估。这比“设置 direct”多出一整套信任决策。§13.4.4 Attestation Limitations

§9 与 §10:扩展是可选能力#

L2 扩展 用途 学习时的注意点
appid 兼容旧 FIDO U2F 凭据 仅有迁移需求才引入
appidExclude 注册时排除已有旧 U2F 凭据 不替代新凭据的正常注册检查
uvm 用户验证方法信息 可用性和隐私要分别考虑
credProps 获取凭据属性,例如 rk 用于了解可发现性;客户端输出不能代替服务器身份验证
largeBlob 凭据关联的大数据存储 非所有认证器支持;不要当作通用应用数据库

调用 credential.getClientExtensionResults() 读取客户端扩展输出。某项输出缺失可能意味着未支持或未执行;根据该扩展的定义处理。客户端输出和认证器签名覆盖的扩展输出不是同一信任层次,不能把所有扩展结果都当作已签名事实。

按问题跳转#

CHAPTER 12

来源、版本与译注

本项目是一份面向 Web 开发者的中文学习文档:整理翻译 WebAuthn Level 2 的核心内容,并配上独立讲解与示例代码。它不是 W3C 授权译本,不是完整逐字翻译,也不构成新的技术规范。

整理与链接核对日期:2026 年 10 月 16 日。若中文解释与规范产生歧义,以对应英文版本为准。浏览器能力和第三方库 API 应按各自版本再次核对。

主规范与原始入口#

来源 版本与用途
用户提供的英文入口 §1.2.2 Authentication 完整保留最初链接,含其原始查询参数
WebAuthn Level 2 W3C Recommendation,2021-04-08;本文主要整理翻译来源
Level 2 固定版本 对照历史语境时使用的不可变版本入口
Level 2 Errata 发布后的勘误;请同时检查规范首页所指向的最新勘误入口
WebAuthn Level 3 现代补充来源;核对时首页标示 W3C Recommendation,2026-08-25
Level 3 固定版本 本次补充所参考的发布版本
编辑草案 可能持续变化;不能与上述发布版不加区分地引用

如何区分文档内容#

**“中文释义”按对应原文重述核心意思,允许调整句式以方便学习。“开发讲解”解释工程上的原因、误区与取舍。“现代补充”**说明 L2 之后的 Passkey 实践或 L3 能力。代码示例用于演示学习,不是规范原文的逐行复刻。

规范中的浏览器内部算法和认证器操作,被整理为职责、数据结构与原文导航;没有声称全部翻译。设备证明格式、扩展和自动化的深层细节请通过章节对照回到原文。

示例与延伸阅读#

本站的学习代码及运行说明可以从完整示例进入。站点本身没有收集、注册或验证读者真实凭据的线上账号系统。

W3C 归属与许可#

本文包含从 Web Authentication: An API for accessing Public Key Credentials — Level 2 复制、翻译或派生的材料;原规范状态为 W3C Recommendation,2021 年 4 月 8 日。

Copyright © 2021 W3C® (MIT, ERCIM, Keio, Beihang). This software or document includes material copied from or derived from Web Authentication: An API for accessing Public Key Credentials — Level 2, https://www.w3.org/TR/webauthn-2/.

保留原作者版权声明:Copyright © 2021 W3C (World Wide Web Consortium)® (MIT (Massachusetts Institute of Technology), ERCIM (European Research Consortium for Informatics and Mathematics), Keio, Beihang). W3C liability、trademark 和 document use 规则适用。原文的规范、状态和完整归属可在英文版首页查看。

现代补充也参考 WebAuthn Level 3。Copyright © 2026 World Wide Web Consortium. This document includes material derived from Web Authentication: An API for accessing Public Key Credentials — Level 3. 参见 W3C document license。

此中文指南用于帮助实现和学习配套软件,不作为替代 W3C 英文规范的技术标准,也不表示得到 W3C 或任何浏览器、认证器厂商的认可。原规范按其许可中列明的条件与免责声明提供。

搜索指南

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

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