先确定失败发生在哪一层:选项生成、浏览器调用、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 |
异常类型可能随平台和具体失败阶段变化;以捕获的错误、请求状态和规范算法共同判断,不能用这张表推断唯一根因。
浏览器诊断片段#
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 结束旧请求:
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。
// 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 记录验证范围。将它作为你的测试起点,然后补充账号绑定、恢复、撤销和部署域名相关的应用测试。
遇到失败时的记录模板#
环境:浏览器 / 操作系统 / 认证器类型
操作:注册 / 有用户名登录 / 无用户名登录
配置:固定 RP ID、预期 origin(不含令牌)
阶段:options / create|get / serialize / verify
外部表现:异常 name、HTTP 状态、请求关联 ID
服务端原因:challenge expired / origin mismatch / ...
复现:是否稳定,换设备、重试或刷新后如何变化一次只改变一个变量。先修复域名与编解码错误,再判断认证器兼容性;不要为排查问题临时关掉验证并把配置带到生产。