浏览器弹出系统窗口、用户完成指纹验证,都不代表你的网站已经确认登录。最终决定来自服务器:使用已保存的公钥,验证本次响应,并确认它属于正确的操作与账号。
英文对照:§7.1 注册新凭据 · §7.2 验证认证断言。以下是按开发职责整理的中文释义与工程补充;不是替代原文的逐步规范算法。
把验证分成三层#
| 层次 | 谁负责 | 本地示例如何处理 |
|---|---|---|
| 请求上下文 | 应用 | 固定来源、会话 Cookie、操作类型、一次性 challenge 与有效期 |
| WebAuthn 结构与密码学 | 验证库 | 解析客户端数据、CBOR / COSE、RP hash、标志、证明与签名 |
| 账号授权与状态 | 应用 | 凭据归属、user handle、唯一性、计数更新、成功后建立会话 |
成熟验证库能减少密码学实现错误,但无法知道你的业务账号属于谁。因此,“库返回成功”仍不能代替账号绑定检查。
注册响应需要核对什么#
按 §7.1 原文阅读时,重点追踪:
- 客户端数据的类型是
webauthn.create;challenge、origin 与服务端预期一致。 - 解码证明对象,验证 RP ID hash、UP;要求 UV 时必须检查 UV。
- 公钥算法属于发出的算法集合;扩展结果按使用策略处理。
- 按证明格式验证,并决定其信任结果是否符合 RP 策略。
- 检查凭据 ID 的账号归属与唯一性,再保存公钥等结果。
本示例明确接受 none 证明策略,因此不要求设备厂商证书。它仍校验注册数据;同时也不声称能证明设备型号、私钥处于硬件内或用户的现实身份。
登录响应需要核对什么#
§7.2 原文可按下面的顺序理解:
- 根据凭据 ID 查找已登记的公钥,确认凭据属于目标用户;有允许列表时还要检查列表约束。
- 无用户名登录需要有效的
userHandle,并确认它和凭据归属指向同一账号。 - 检查类型
webauthn.get、challenge、origin、RP ID hash、UP 与所要求的 UV;处理扩展。 - 验证
authenticatorData || SHA-256(clientDataJSON)的签名,评估计数器。 - 只有全部通过后,更新状态并建立登录会话。
|| 在这里表示二进制拼接,不是 JavaScript 的逻辑或。clientDataJSON 要使用响应里的原始字节计算哈希,不能先解析 JSON 再重新序列化。
对照可运行代码#
配套服务端源文件使用以下调用进行登录验证;API 已按锁定版本 @simplewebauthn/server 14.0.3核对:
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 为什么要先消费#
示例在进入异步验证库之前,先同步取出并删除当前会话的待验证操作:
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 官方说明
下一步:运行完整示例,再回来看每个检查分别在哪里执行。