指南/第 06 章

CHAPTER 06 / 12

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

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

英文对照:§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 官方说明

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

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

搜索指南

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

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