指南/第 10 章

CHAPTER 10 / 12

调试、错误与测试

先确定失败发生在哪一层:选项生成、浏览器调用、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 / ...
复现:是否稳定,换设备、重试或刷新后如何变化

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

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

搜索指南

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

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