---
url: 'https://www.corbado.com/zh/blog/webauthn-errors'
title: '生产环境 WebAuthn 错误终极指南（2026）'
description: '了解在生产环境中常见的 WebAuthn 错误（如 NotAllowedError）代表什么，以及如何根据操作类型、时间和平台上下文对它们进行分类。'
lang: 'zh'
author: 'Vincent Delitz'
date: '2026-07-03T07:08:46.451Z'
lastModified: '2026-07-03T07:10:43.720Z'
keywords: 'webauthn 错误, NotAllowedError, AbortError, SecurityError, WebAuthn 故障排除, 通行密钥错误分类, 条件创建错误, ASAuthorizationError, ASAuthorizationError 代码, androidx.credentials, 凭证管理器通行密钥错误, iOS'
category: 'WebAuthn Know-How'
---

# 生产环境 WebAuthn 错误终极指南（2026）

## 1. 简介

在生产环境中，WebAuthn 错误令人困惑，因为浏览器暴露的一小部分 `DOMException` 名称（如 `NotAllowedError`）可能代表多种根本原因。此外，绝大多数“错误”——在经过优化的大规模部署中通常超过 95%——实际上是**预期行为**（用户中止了操作系统的通行密钥提示）。

> 重要提示：出于隐私原因，浏览器不会区分是用户主动取消还是不存在通行密钥。但是，在某些情况下，只要有足够的上下文，无论是在 Web 还是在原生平台上，都可以使用诸如时间等信号来区分这些情况。

如果您想了解这些名称的规范定义，请从 [MDN `DOMException`](https://developer.mozilla.org/en-US/docs/Web/API/DOMException) 开始。有关导致这些异常的 WebAuthn 特定条件（以及浏览器需要执行的内容），请参阅 [W3C Web Authentication 规范](https://www.w3.org/TR/webauthn-3/)。

如果您将所有错误都视为“错误（bugs）”，您将会采取错误的行动：

- 您会用正常的取消操作污染您的错误指标
- 您会错过隐藏在大量 `NotAllowedError` 中的真正回归
- 您会发布让用户感到困惑的 UI，而不是帮助他们恢复

在本文中，我们解答：

- 最常见的 WebAuthn 错误名称在真实流量中通常意味着什么？
- 如何将 `NotAllowedError` 消除歧义并划分为可操作的类别（取消 vs 超时 vs 可用性）？
- 为什么相同的错误根据操作（条件 UI 登录 vs 模态登录 vs 通行密钥创建 vs 条件创建）意味着不同的事情？
- 您应该捕获什么最低限度的上下文，以便“失败了”成为一个可重现的问题？

## Key Facts

- `NotAllowedError` 是一个**表面信号**，而不是根本原因。根据上下文，它可能意味着取消、超时、“没有本地凭证”或缺少用户激活。
- **操作类型改变了含义。** 同样的 `NotAllowedError` 在条件 UI 登录、模态登录、手动通行密钥创建、条件创建和自动触发的附加期间意味着不同的事情。
- **从操作开始计时**是最被低估的信号：立即（`<1s`）、用户取消（1-15s）和超时（30s+）是根本不同的类别。
- `AbortError` 通常是生命周期/并发问题（导航、重新渲染、多个正在进行的请求）。
- `SecurityError` 几乎总是配置/上下文问题，在成熟的生产部署中很少见。
- “浏览器错误名称”和“服务器验证拒绝”是不同的层。将服务器拒绝作为显式代码跟踪，而不是作为通用的客户端失败。
- **原始错误名称单独不具有可操作性。** 始终在 `error.name` 旁边捕获操作类型、时间和平台上下文，以便您可以将错误分类到可以实际修复的类别中。
- **“环境”不仅仅是浏览器 + 操作系统。** 要真正了解错误，您需要跟踪完整的组合：操作系统版本、客户端（浏览器/应用版本）、身份验证器设置（例如 iCloud/GPM 状态）和具体的硬件型号。
- **登录失败是 P1，创建失败是 P2。** 虽然创建错误（P2）通常由于用户放弃而具有更高的数量，但登录错误（P1）会阻止访问并需要立即发出警报。

## 2. 生产环境速查表

如果您只需要快速映射以解除调试阻塞，请从下表开始。它偏向于团队在仪表板和支持工单中实际看到的内容。

| **`error.name`**    | **在生产环境中通常意味着什么**                                                                                              | **如何检查以确认**                                                                   | **首要行动（UX + 工程）**                                                                |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `NotAllowedError`   | 用户关闭了工作表、超时或可用性不匹配，都折叠到一个类别中。这是生产中最大的错误类别。 | 到错误的时间、是否出现 QR/混合 UI、仪式是否从真实用户操作开始 | 视为预期：恢复 UI + 显示后备方案                                                      |
| `AbortError`        | 您的应用（或浏览器）中止了仪式                                                                                       | 仪式期间导航/重新渲染；并发的 WebAuthn 调用；`AbortController.abort()`     | 强制实施一个正在进行的请求；防止路由更改；将中止作为正常的控制流处理          |
| `SecurityError`     | 不允许上下文/策略                                                                                                           | 来源 + RP ID 策略；iframe/嵌入；HTTPS；功能策略                               | 修复 RP ID/来源配置；验证嵌入策略；确保安全上下文                 |
| `InvalidStateError` | 状态不匹配（通常是重复注册）                                                                                        | 注册与登录；`excludeCredentials`；身份验证器上的现有凭证              | 视为“已注册”；调整 UX 路径；修复选项生成                                 |
| `ConstraintError`   | 无法满足要求                                                                                                      | `authenticatorAttachment`、`userVerification`、常驻密钥要求                       | 放宽限制或提供替代路径/后备方案。示例：Android 上缺少屏幕锁定 |
| `DataError`         | 输入格式错误/不一致                                                                                                    | base64url 编码；id/质询/用户句柄格式                                           | 修复编码/序列化；在选项生成中添加验证                                    |
| `NotSupportedError` | 平台/浏览器不支持您的请求                                                                                      | 操作系统/浏览器版本；功能检测假设                                              | 立即回退；记录片段；避免为不受支持的环境显示通行密钥 CTA     |
| `UnknownError`      | 平台/身份验证器以通用方式失败                                                                                       | 操作系统更新后的激增；设备构建；凭证管理器提供商问题                      | 适合重试的 UX；捕获构建号；调查片段激增                               |

很容易错过的一点是：相同的 `error.name` 可能根据**操作类型**意味着非常不同的事情。在阅读以下部分时，请牢记操作上下文。在实践中，通行密钥创建（注册）错误通常远远超过登录错误——上表适用于两者，但创建是大部分数量集中的地方。

接下来，我们将更深入地探讨 `NotAllowedError`，因为这是您会看到最多、也是团队最常误解的一个。

## 3. NotAllowedError: 操作超时或不被允许

`NotAllowedError` 往往看起来像“通行密钥失败”，但通常是平台告诉您用户没有完成 OS UI。关键是将其拆分为您可以采取行动的类别。

**您将在浏览器控制台中看到的内容：**

| **来源**     | **错误消息**                                                                                                                                    |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Chrome, Edge   | `NotAllowedError: The operation either timed out or was not allowed. See: https://www.w3.org/TR/webauthn-2/#sctn-privacy-considerations-client.`     |
| Safari, WebKit | `NotAllowedError: The request is not allowed by the user agent or the platform in the current context, possibly because the user denied permission.` |
| Safari, WebKit | `NotAllowedError: This request has been cancelled by the user.`                                                                                      |
| Chrome, Edge   | `NotAllowedError: The operation is not allowed at this time because the page does not have focus.`                                                   |
| Safari, WebKit | `NotAllowedError: The document is not focused.`                                                                                                      |
| Firefox        | `NotAllowedError: Operation failed.`                                                                                                                 |

所有这些都会表现为 `error.name === "NotAllowedError"`。`error.message` 因浏览器引擎和潜在原因而异，但结果是一样的：仪式没有完成。

这适用于**登录和通行密钥创建**。在登录期间（条件 UI、带有或不带有 allowList 的模态），`NotAllowedError` 通常意味着用户没有完成仪式。在通行密钥创建期间，相同的错误由于不同的原因浮现：用户解散了创建对话框（条件创建未起作用），或者页面在自动触发的附加过程中失去了焦点。操作类型改变了错误的含义以及您应该如何处理它。

**时间往往是一个被低估的信号。** 点击后不到一秒钟的错误通常是立即拒绝（环境无法做到、文档未获得焦点、缺少功能）。几秒钟后的错误是用户取消（他们看到了对话框并决定不继续）。30 秒以上的错误是超时。在原生平台上，时间尤为重要：身份验证器往返、生物识别提示和凭证管理器交接都有特征持续时间，可帮助您区分“不工作”和“用户走开了”。您仍然无法轻松区分是否存在通行密钥。

### 3.1 使用上下文消除歧义

您不需要完美的信号。您需要足够的上下文来避免以相同方式处理每个 `NotAllowedError`。iOS/Safari 在下面得到了特别关注，因为它具有独特的约束（早期版本中的用户激活要求），但在原始错误量方面，Windows 和 Chromium 浏览器通常会生成比任何其他平台更多的 `NotAllowedError`。这些信号通常能帮您解决 80% 的问题：

| **信号**                                                   | **可能意味着什么**                                                                                                                                                                                                                                                                               | **下一步该做什么**                                                                                                                                                                           |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 立即失败（`<1s`）                                    | 环境拒绝：无能力、文档未获得焦点、条件创建表面不可用                                                                                                                                                                                               | 检查功能检测；确保文档具有焦点；验证此平台上是否支持操作                                                                                            |
| 快速取消（1-3s）                                           | 突然的提示 / 没有上下文                                                                                                                                                                                                                                                                     | 更改提示时间；在取消后添加冷却时间                                                                                                                                               |
| 用户长度的取消（3-15s）                                   | 用户看到对话框并选择不继续                                                                                                                                                                                                                                                     | 预期 UX；恢复 UI + 显示后备方案                                                                                                                                                       |
| 超时（30s+）                                               | 仪式在没有用户操作的情况下超时                                                                                                                                                                                                                                                           | 归类为“未完成”；考虑提示是否被注意到                                                                                                                         |
| 在失败前出现 QR/混合 UI                          | 此设备上没有可用的本地凭证。注意：可靠地在它发生之前检测 QR 码决策需要一个了解当前设备上是否存在可用凭证的[通行密钥智能（passkey intelligence）](https://docs.corbado.com/corbado-connect/features/passkey-intelligence)层。 | 限制通行密钥提议；明确“使用手机”选项；减少突然出现的 QR 码                                                                                                                            |
| 集中在 iOS/Safari 并在没有点击/点击的情况下触发 | 缺少用户激活                                                                                                                                                                                                                                                                          | 从真实的用户手势开始仪式                                                                                                                                                   |
| 在条件创建或自动触发的附加期间           | 自动填充不可用、凭证已存在或页面失去焦点。条件创建错误可能在功能启动时突然大量出现，使其在一夜之间成为最大的错误来源之一。                                                                    | 请参阅条件创建；检查文档可见性状态；在尝试调用之前，使用 `getClientCapabilities()` 验证 `conditionalCreate` 支持 |

这也是为什么 `NotAllowedError` 很少应该对用户可见的原因。这不是用户可以采取行动的消息。

上下文分类也是成功率出现最明显分歧的地方。[Corbado Passkey Benchmark 2026 通行密钥身份验证成功率分析](https://www.corbado.com/passkey-benchmark-2026/passkey-authentication-success-rate)测量了大型 B2C 部署中，2026 年第一季度针对未知设备标识符优先流程的完成率为 55–95%，而已知设备返回的完成率为 95–99%。
iOS Web 标识符优先的完成率为 85–95%（% CDA 为 0–5%），Android Web 为 70–85%（% CDA 为 5–10%），macOS Web 为 70–85%（% CDA 为 10–15%），而 Windows Web 的标识符优先完成率仅为 45–60%，在 Windows 11 上 % CDA 为 55–65%，在 Windows 10 上为 40–55%。在不区分已知与未知设备上下文的情况下读取 `NotAllowedError` 数量，会将两种根本不同的成功机制混为一谈。

在生产环境中有一点很容易被忽略：有些用户代理可能会将超时显示为 `TimeoutError`，但许多团队仍然会在仪表板中看到超时被归入 `NotAllowedError`。无论哪种方式，都要将超时视为“仪式未完成”，并结合时间与上下文进行归类。

### 3.2 UX 处理：使取消成为正常退出

当操作系统工作表被关闭或超时时，您的 UI 应立即恢复并优雅地做出反应。一套实用的规则：

- 恢复登录 UI（不要让加载项一直运行）
- 保留标识符状态（不要强迫重新输入）
- 在同一屏幕上显示可见的后备方案
- 避免为预期结果显示可怕的横幅

除了基础知识：

- 将第一次中止视为正常，并提供重试和冷静解释。只有在第二次中止后，您才应建议使用后备选项。
- 允许在冷却前出现最多三次创建提示，以便惊讶的用户有第二次机会。
- 将创建和登录之间的错误计数分开，以便您进行同类比较。
- 按操作系统、浏览器和设备细分错误率，以便您发现摩擦实际存在的地方。

如果您的“取消”真正很高，下一步是修复它们背后的根本原因：提示时间、QR 惊讶和低可用性。

### 3.3 减少 NotAllowedError 的工程修复

从快速移动指标的更改开始：

- 修复提示时间和用户激活（特别是在 iOS/Safari 上）。
- 减少 QR/混合惊讶。
- 限制提议，以便通行密钥仅在可能成功时显示。
- 使用避免在没有本地凭证存在时提示的模式。
- **处理网络不稳定：** 验证期间的网络错误通常表现为通用的客户端失败。为您的遥测日志实施脱机队列，以便在用户在仪式期间断开连接时不会丢失错误数据。
- **使用检测 API 限制仪式** 以免环境失败膨胀您的 `NotAllowedError` 类别。从 `isUVPAA()` 作为最基本的限制开始，然后使用 `getClientCapabilities()` 进行更精细的检查（条件创建、条件获取、混合传输、平台身份验证器）。请注意，检测 API 可能会随着操作系统更新而崩溃：iOS 26.2 发布了一个 WebKit 错误，导致 `isUVPAA()` 在所有基于 WKWebView 的浏览器上返回 `false`，即使通行密钥工作正常，也会导致 10-25% 的 iOS 用户出现突发的 `NotAllowedError` 峰值。

### 3.4 关于不断演变的错误名称的说明

错误名称是一个不断移动的目标。有正在进行的提案为 WebAuthn 错误添加更细粒度的分类（例如，将“没有可用凭证”与“用户取消”分开）。截至 2026 年 2 月，这还没有在任何浏览器中实现，因此仍然值得根据上下文和时间构建自己的原因分类。如果您想跟踪这项工作，请参阅 [WebAuthn issue #2062](https://github.com/w3c/webauthn/issues/2062) 和 ["New Error Codes" explainer](https://github.com/w3c/webauthn/wiki/Explainer%3A-New-Error-Codes-%282024-Edition%29)。

其余的错误名称频率较低，但在它们出现时仍然值得理解。

## 4. AbortError: 操作已中止

与 `NotAllowedError` 相比，`AbortError` 的数量不多，但当它出现时，它具有很高的诊断意义：它通常意味着仪式未完成，因为您的应用程序使请求失效——发生了导航、状态改变，或开始了第二个请求。

**您将在浏览器控制台中看到的内容：**

| **来源**     | **错误消息**                                         |
| -------------- | --------------------------------------------------------- |
| Chrome, Edge   | `AbortError: The operation was aborted.`                  |
| Chrome, Edge   | `AbortError: Aborted by AbortSignal.`                     |
| Firefox        | `AbortError: signal is aborted without reason`            |
| Firefox        | `AbortError: Operation timed out.`                        |
| Safari, WebKit | `AbortError: The user aborted a request.`                 |
| Chrome         | `AbortError: CredentialContainer request is not allowed.` |

常见的生产原因包括：

- 多个并发的 WebAuthn 调用（两个提示竞争）
- 仪式进行期间的路由改变/重新渲染
- 在重试或状态清理期间调用 `AbortController.abort()`

要修复它，请专注于使仪式成为一个“关键部分”：

- 任何时候只允许一个正在进行的请求
- 在仪式期间阻止导航（或者干净地取消并恢复 UI）
- 将中止视为正常的控制流：显示重试按钮和后备方法

如果您看到 `AbortError` 集中在嵌入的界面或多域应用程序中，接下来要检查的类别是 `SecurityError`。

## 5. SecurityError: 在有 TLS 证书错误的网站上不支持 WebAuthn

`SecurityError` 是浏览器在告诉您：“这个上下文不允许执行您所请求的操作。”在实践中，这几乎总是配置问题，而不是用户行为。在成熟的生产部署中，`SecurityError` 很少见，因为这些问题通常在集成测试期间就会被发现。如果它出现在生产环境中，通常意味着在没有正确进行 WebAuthn 配置的情况下，添加了新域名、嵌入上下文或部署目标。

常见原因包括：

- RP ID / 来源不匹配（多域设置）
- 跨域嵌入限制（iframe）
- 不安全的上下文（非 HTTPS）或被阻止的权限/策略
- `.well-known/webauthn` 或 `.well-known/assetlinks.json` 配置错误、丢失或暂时不可用。如果浏览器获取这些文件时的关键窗口内发生网络问题，将导致失败。一个常见的盲点：如果您的主页因维护而下线，well-known 文件也会离线，这会破坏依赖它们的所有依赖方的通行密钥仪式。

**您将在浏览器控制台中看到的内容：**

| **来源**      | **错误消息**                                                                                                                |
| --------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| Chrome, Edge    | `SecurityError: WebAuthn is not supported on sites with TLS certificate errors.`                                                 |
| 任何浏览器     | `SecurityError: The relying party ID is not a registrable domain suffix of, nor equal to the current origin's effective domain.` |
| Chrome (iframe) | `SecurityError: The 'publickey-credentials-create' feature is not enabled in this document.`                                     |

在生产环境中，`SecurityError` 很少见——这些问题几乎总是在集成测试中被捕获。当它们确实出现时，TLS 证书错误是最常见的遗留问题。

最快的调试循环是：

- 记录您使用的 origin 和 RP ID 输入
- 在相同的上下文中重现（顶层 vs iframe，生产域 vs 暂存）
- 如果您嵌入登录，请确认配置了 permissions policy（例如 `publickey-credentials-create` / `publickey-credentials-get`）：[MDN Permissions-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Permissions-Policy/publickey-credentials-create)
- 验证您的域策略
- 如果使用 iframes，请验证 feature policies

一旦 `SecurityError` 被处理，下一个需要认真对待的类别是一组通常表明实现错误的错误：`InvalidStateError`、`ConstraintError` 和 `DataError`。

## 6. InvalidStateError, ConstraintError, DataError: 视为实现错误

在成熟的通行密钥实现中，这些错误应该很少见。当它们出现时，通常表明对某个片段的选项生成是错误的，或者流程处于错误状态。

### 6.1 InvalidStateError: "已使用依赖方注册的凭证"

**您将在浏览器控制台中看到的内容：**

| **来源**     | **错误消息**                                                                                                                                    |
| -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| Safari, WebKit | `InvalidStateError: The user attempted to register an authenticator that contains one of the credentials already registered with the relying party.` |
| Chrome, Edge   | `InvalidStateError: At least one credential matches an entry of the excludeCredentials list in the platform attached authenticator.`                 |
| Chrome, Edge   | `InvalidStateError: A request is already pending.`                                                                                                   |
| Firefox        | `InvalidStateError: An attempt was made to use an object that is not, or is no longer, usable`                                                       |

典型含义：

- 注册：您试图创建一个已经存在的凭证（重复）
- 登录：不太常见；通常意味着流程/状态对于平台来说是不一致的

实际处理：

- 如果它发生在手动注册中，请将其视为“已注册”并相应地路由
- 确保 `excludeCredentials` 列出了该用户所有现有的 credential IDs，以便身份验证器能够检测到重复项
- 在条件创建期间，`InvalidStateError` 是预期的，应被忽略：它意味着提供商中已存在通行密钥。`NotAllowedError` 和 `AbortError` 在条件创建期间也是如此（请参阅 [conditional create on Chrome](https://developer.chrome.com/docs/identity/webauthn-conditional-create)）

### 6.2 ConstraintError

典型含义：身份验证器无法满足您请求的约束。

常见触发器：

- 缺少设备屏幕锁定（尤其是 Android）：平台需要生物识别或 PIN 验证，但设备未配置锁定屏幕
- 过于严格的 `authenticatorAttachment` 或 resident key 假设
- 在不可用的片段中实施硬性 `userVerification` 要求

修复：放宽限制（在可接受的情况下）或提供替代路径。对于缺少屏幕锁定的情况，请考虑检测此情况并引导用户，而不是默默失败（Android）。

### 6.3 DataError

典型含义：输入格式错误或不一致。

常见触发器：

- 编码错误（base64 与 base64url）
- 无效的 credential id / 质询格式

修复：在生成 WebAuthn 选项的边界处验证并规范化输入。实际上，`DataError` 在成熟的生产系统中实际上是不存在的——如果您的选项生成经过测试，您在仪表板中就不会看到这个问题。

如果这些错误得到控制，下一个问题是覆盖率：由于环境无法按照您的预期执行 WebAuthn 而导致用户失败？

## 7. NotSupportedError: 用户代理不支持公钥凭证

`NotSupportedError` 是一个覆盖率信号，而不是可靠性信号。它通常意味着某个片段无法执行您的请求（操作系统/浏览器太旧、缺少功能、功能未启用）。

**您将在浏览器控制台中看到的内容：**

| **来源**            | **错误消息**                                                                              |
| --------------------- | ---------------------------------------------------------------------------------------------- |
| Chrome, Edge          | `NotSupportedError: The user agent does not support public key credentials.`                   |
| Firefox               | `NotSupportedError: Resident credentials or empty 'allowCredentials' lists are not supported.` |
| Chrome, Edge, Firefox | `TypeError: PublicKeyCredential.parseCreationOptionsFromJSON is not a function`                |
| Chrome, Edge, Firefox | `TypeError: PublicKeyCredential.parseRequestOptionsFromJSON is not a function`                 |
| Chrome, Edge, Firefox | `TypeError: credential.toJSON is not a function`                                               |
| Safari                | `TypeError: Can only call PublicKeyCredential.toJSON on instances of PublicKeyCredential`      |

前两个是真正的 `NotSupportedError` DOMException。`TypeError` 条目在技术上是不同的异常类型，但代表了同类问题：浏览器或环境不支持您请求的操作。在实践中，JSON 序列化 `TypeError` 家族比 `NotSupportedError` DOMException 本身常见得多（见下文）。

常见原因包括：

- 不支持基本 WebAuthn 的较旧浏览器/操作系统版本
- 请求该平台上不可用的 WebAuthn 功能
- 尝试在不支持的设备上进行特定于平台的流程

**JSON 序列化系列是生产中 `NotSupportedError` 级失败的最大来源。** 技术上，这些表面表现为 `TypeError`（缺少方法），而不是 `DOMException`，但这正是您会遇到它们的地方。两个截然不同的根本原因：

1. **浏览器不支持 WebAuthn JSON 序列化方法。** 浏览器拥有 `navigator.credentials` 但没有 `PublicKeyCredential.parseCreationOptionsFromJSON` / `parseRequestOptionsFromJSON`。这占该错误系列的约 90%，集中在较旧的 Safari 和 Chrome 版本中。如果您的客户端库依赖这些方法而没有后备方案，则会产生大量的错误量。
2. **密码管理器扩展破坏了 `.toJSON()`。** Bitwarden、LastPass 或 1Password 等扩展可以拦截仪式并返回一个看起来像凭证的对象，但它不是真正的 `PublicKeyCredential` 实例。在它上面调用 `.toJSON()` 要么抛出异常，要么返回未定义，或者对象完全是 `null`。这大约占该系列的 10%，但因为错误消息因浏览器而异，因此特别难以调试（Safari：“Can only call on instances of PublicKeyCredential”；Firefox：“does not implement interface PublicKeyCredential”）。

处理应该是直接而快速的：

- 立即回退到备用密码/OTP
- 记录片段以便量化覆盖缺口
- 避免在会持续失败的片段上显示通行密钥 CTA

如果覆盖率看起来不错，但在特定细分市场中仍发生失败，您可能会遇到表现为 `UnknownError` 的平台层问题。

## 8. UnknownError: 在与凭证管理器对话时发生未知错误

`UnknownError` 是一个统称，用于处理那些无法清晰归入其他类别的身份验证器/操作系统失败。它通常是暂时的，但也可能在操作系统更新后激增。

**您将在浏览器控制台中看到的内容：**

| **来源**         | **错误消息**                                                                                                                       |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------- |
| Chrome (Android)   | `UnknownError: An unknown error occurred while talking to the credential manager.`                                                      |
| 任何浏览器        | `UnknownError: The operation failed for an unknown transient reason.`                                                                   |
| 任何浏览器        | `UnknownError: Either the device has received unexpected request data, or the device has been reconfigured since the request was made.` |
| 任何浏览器        | `UnknownError: Something went wrong.`                                                                                                   |
| Chrome (LastPass)  | `TypeError: Cannot use 'in' operator to search for 'type' in null`                                                                      |
| Safari (LastPass)  | `TypeError: null is not an Object. (evaluating 'key in input')`                                                                         |
| Chrome (Bitwarden) | `FallbackRequested`                                                                                                                     |

实际处理：

- 使用重试友好的 UX（不要“责怪用户”）
- 尽可能捕获 OS/内部版本号和凭证管理器/提供程序上下文
- 注意操作系统更新后的片段特定激增

不完全属于任何 `DOMException` 类别的另一个小众错误来源是：**密码管理器浏览器扩展**（如 Bitwarden、LastPass、1Password 等）可以拦截 WebAuthn API 调用并返回非标准响应。虽然与用户取消相比数量较小，但它们值得跟踪，因为它们始终如一地影响特定的用户群体并且症状令人困惑：返回的凭证对象上缺少方法、意外的错误类型，或者是与任何文档中描述的 WebAuthn 错误都不匹配的格式错误响应。这些错误通常表现为 `UnknownError` 或未分类异常。如果发现某个特定浏览器上错误激增，且没有操作系统级别的解释，请检查是否涉及了凭证管理器扩展。

到目前为止，我们已经介绍了 Web 浏览器的错误名称。但是，如果您还发布原生应用，情况会有所不同——在某些方面甚至会更好。

## 9. 关于原生应用（iOS 和 Android）的一点说明

上面的内容涵盖了 Web 浏览器。原生应用——带有 ASAuthorization 框架的 iOS，带有 Credential Manager 的 Android——共享相同的基本错误类别，但在重要方面有所不同：

1. **“无凭证”是一个明确的信号。** 在网页端，出于隐私原因，浏览器将“无可用凭证”和“用户取消”合并到同一个 `NotAllowedError` 中。在原生应用中，如果在 iOS (`ASAuthorizationController`) 上使用 `preferImmediatelyAvailableCredentials`，或者在 Android (`GetCredentialRequest`) 上使用 `setPreferImmediatelyAvailableCredentials(true)`，系统会指示操作系统只展示设备上已有的凭证，如果没有，则立即失败。这就为您提供了一个浏览器无法提供的干净的“无凭证”返回结果。

2. **凭证提供程序的处于可见状态。** 原生平台在某些情况下可以告诉您未安装、未配置或未将凭证提供程序（如 Google Password Manager、iCloud Keychain、1Password 等）设置为默认值并对此作出反应。在 Web 上，此信息隐藏在不透明的 `NotAllowedError` 消息后面。

3. **错误消息更加具体。** 由于用户已安装该应用，从而与依赖方建立了信任关系，因此操作系统将提供更多诊断详细信息。适用于 Web 浏览器以保持模糊的隐私考量在应用已经存在于设备上的情况下并不以相同的方式适用。iOS 以用户设备语言返回本地化消息。Android 会返回带有原因链的结构化错误类型。这使调试更容易，但也意味着您的错误处理必须考虑到本地化和特定于平台的错误格式。

### 9.1 iOS (ASAuthorization 框架)

iOS 通过 ASAuthorization 框架公开通行密钥错误。所有错误都在 `authorizationController(controller:didCompleteWithError:)` 委托回调中以 `NSError` 对象的形式到达。

**按域 + 代码进行分类，而不是按消息字符串。** 主要的错误域是 `com.apple.AuthenticationServices.AuthorizationError`（`ASAuthorizationError.errorDomain`）。通过 `let nsError = error as NSError` 转换错误并匹配 `.domain` 和 `.code`。在生产环境中切勿对 `.localizedDescription` 进行匹配——Apple 的消息已本地化为 30 多种语言，并可能随着 OS 版本的升级而改变。下面列出的消息字符串对于在日志中识别错误很有用，但它们不能用作分类标准。

**公共 ASAuthorizationError 代码：**

| **代码** | **名称**                                | **自起** | **意味着什么**                                                                                                                                                                                                                                                                                                     |
| -------- | --------------------------------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1000     | `Unknown`                               | iOS 13    | 不应该在生产中出现。通用总括。                                                                                                                                                                                                                                                                   |
| 1001     | `Canceled`                              | iOS 13    | 用户关闭了通行密钥工作表。总体上最常见的错误 - 相当于 `NotAllowedError`。清晰信号，空的 `userInfo` 且没有根本错误。                                                                                                                                                           |
| 1002     | `InvalidResponse`                       | iOS 13    | 框架级损坏。在实践中很少见。                                                                                                                                                                                                                                                                         |
| 1003     | `NotHandled`                            | iOS 13    | 没有提供程序处理请求。检查权利和凭证提供程序配置。                                                                                                                                                                                                                            |
| 1004     | `Failed`                                | iOS 13    | 通用失败。`localizedDescription` 包含实际原因（例如“标识符为 X 的应用程序未与域 Y 关联”）。`userInfo` 可能包含一个 `FailureReason` 字符串，但 `NSUnderlyingErrorKey` 并非总是填充 - 域关联失败会为根本错误返回 nil。 |
| 1005     | `NotInteractive`                        | iOS 15    | 当使用 `preferImmediatelyAvailableCredentials` 且没有可用凭证时。这是清晰的“未找到”信号 - 相当于“此设备上不存在通行密钥”的 iOS 版本。没有显示 UI。                                                                                                                        |
| 1006     | `MatchedExcludedCredential`             | iOS 18    | 此设备上已存在此 RP 的通行密钥。针对重复检测的清晰信号 - 空的 `userInfo`，没有 `NSUnderlyingErrorKey`。无需字符串匹配即可工作。                                                                                                                                |
| 1007     | `CredentialImport`                      | iOS 18.2  | 凭证导入失败。                                                                                                                                                                                                                                                                                             |
| 1008     | `CredentialExport`                      | iOS 18.2  | 凭证导出失败。                                                                                                                                                                                                                                                                                             |
| 1009     | `PreferSignInWithApple`                 | iOS 26    | 用户偏好使用“通过 Apple 登录”而不是通行密钥。iOS 26 中的新功能。                                                                                                                                                                                                                                                          |
| 1010     | `DeviceNotConfiguredForPasskeyCreation` | iOS 26    | 设备缺少密码或 iCloud 钥匙串配置。已知 iOS 26 模拟器错误：即使配置正确也返回 1010（在物理设备上未重现）。                                                                                                                                         |

生产环境中最重要的区别：**自 iOS 18 起，重复凭证返回其自己的错误代码 1006 (`MatchedExcludedCredential`)。** 在 iOS 17 及更早版本中，重复凭证隐藏在代码 1004 (`Failed`) 内部。在 iOS 18+ 上，区别是结构性的（不同的错误代码），而不是文本上的。

**常见运行时错误（日志级参考）：**

这些消息会出现在特定失败场景的 `localizedDescription` 或 `userInfo` 中。请使用它们进行日志搜索和调试，而不要用作程序化的分类。

| **消息（英语区域）**                                                    | **底层代码** | **注意**                                                                                                                                      |
| ------------------------------------------------------------------------------- | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| `Application with identifier <TeamID.BundleID> is not associated with domain X` | 1004 (`Failed`)     | 应用程序的 Associated Domains 权利与依赖方不匹配。修复服务器上的 `apple-app-site-association` 文件。            |
| `Couldn't communicate with a helper application.`                               | 1004 (`Failed`)     | 凭证提供商扩展无法响应。瞬态 - 适合重试。                                                         |
| `Request already in progress for specified application identifier.`             | 1004 (`Failed`)     | 在有一个请求挂起时，触发了重复的 ASAuthorization 请求。应用程序中的竞争条件。                                                |
| `Stolen Device Protection is enabled and biometry is required.`                 | 1004 (`Failed`)     | iOS 17+ 遭窃设备保护会在陌生地点阻止生物识别验证。开发人员无法对此进行处理，但值得向用户显示。 |
| `(AuthenticationServicesCore.ASCABLEClient.ClientError error 2.)`               | Separate domain     | 跨设备认证 (hybrid/CABLE) 蓝牙握手失败。                                                                         |
| `(AuthenticationServicesCore.ASCABLEClient.ClientError error 3.)`               | Separate domain     | 跨设备认证蓝牙连接失败。                                                                                       |

**本地化的“无凭证”消息（代码 1005）：**

当设置了 `preferImmediatelyAvailableCredentials` 且不存在通行密钥时，iOS 会返回代码 1005 (`NotInteractive`)，并附带一条采用用户设备语言的本地化消息。这是原生应用程序独有的 - Web 浏览器从不公开此信号。该消息始终以 `The operation couldn't be completed.` 开头，后跟本地化文本：

| **语言**          | **消息**                                                                          |
| --------------------- | ------------------------------------------------------------------------------------ |
| 简体中文  | `没有可用于登录的凭证。`                                                             |
| 越南语            | `Không có sẵn thông tin để đăng nhập.`                                               |
| 阿拉伯语                | `لا تتوفر بيانات اعتماد لتسجيل الدخول.`                                              |
| 西班牙语               | `No hay ninguna credencial disponible para iniciar sesión.`                          |
| 繁体中文 | `沒有可用於登入的憑證。`                                                             |
| 韩语                | `로그인을 위한 자격 증명이 없습니다.`                                                |
| 法语（加拿大）       | `Aucun identifiant disponible pour la connexion.`                                    |
| 葡萄牙语（巴西）   | `Nenhuma credencial disponível para login.`                                          |
| 法语（法国）       | `Aucune information d'identification n'est disponible pour procéder à la connexion.` |
| 泰语                  | `ไม่มีข้อมูลประจำตัวสำหรับเข้าสู่ระบบ`                                               |
| 意大利语              | `Non ci sono credenziali disponibili per l'accesso.`                                 |
| 荷兰语                | `Geen inloggegevens beschikbaar.`                                                    |
| 日语              | `ログイン用の資格情報がありません。`                                                 |
| 土耳其语              | `Oturum açmak için kullanılabilecek kimlik bilgisi yok.`                             |

英语语言环境设备通常会在 API 层面解决“没有凭证”的问题，然后 ASAuthorization 框架才会返回本地化的错误，这就是为什么上面没有显示英语变体的原因。在编程时，始终匹配代码 1005，而不是解析这些字符串。

### 9.2 Android (Credential Manager API)

Android 通过凭证管理器 API (`androidx.credentials`) 公开通行密钥错误。错误消息包括主要消息，通常还包括具有其他详细信息的 `cause`。与 iOS 相比，Android 提供了结构化程度更高的错误类型以及更明确的配置问题原因。

**用户取消和凭证检测：**

| **错误**                                         | **注意**                                                                                                                                                                                                 |
| ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `User cancelled the operation`                    | 用户关闭了通行密钥提示。相当于 `NotAllowedError`。注意：凭证管理器还通过不同的代码路径返回 `User canceled the request`（美国拼写）- 两者是相同的。 |
| `Excluded credential matches existing credential` | 通行密钥已存在此凭证 ID。相当于 `InvalidStateError`。与 iOS 不同，该消息不同于用户取消。                                                          |
| `No create options available.`                    | 没有符合条件的凭证提供商能够处理创建请求。通常表示 Google Play 服务已过时，或者没有凭证提供商支持创建通行密钥。                                    |

**配置和安全错误：**

| **错误**                                                                      | **注意**                                                                                                                                         |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Passkeys not supported for this app`                                          | 缺少 Digital Asset Links (`assetlinks.json`) 或不包含该应用程序的签名证书指纹。相当于 `SecurityError`。 |
| `Https failed: respCode=301, url=https://<domain>/.well-known/assetlinks.json` | `assetlinks.json` 文件返回了重定向而不是 HTTP 200。Android 要求在精确 URL 获取该文件而无重定向。                  |
| `The incoming request cannot be validated`                                     | 凭证管理器无法对照数字资产链接验证请求。                                                                     |
| `RP ID cannot be validated.`                                                   | WebAuthn 选项中的依赖方 ID 与 `assetlinks.json` 不匹配。                                                                    |
| `Screen lock is missing.`                                                      | 设备上未配置 PIN 码、图案或生物识别功能。通行密钥要求验证用户。相当于 `ConstraintError`。                     |
| `Cannot find an eligible account.`                                             | 设备上没有符合创建通行密钥条件的 Google 帐户（罕见，通常是自定义的对公企业设置）。                                      |

**平台和身份验证器错误：**

| **错误**                                                                                | **注意**                                                                                                                                                                                                                                          |
| ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Unsuccessful result from folsom activity.`                                              | Google Play 服务内部错误。“Folsom”是针对通行密钥操作的一个 GMS 组件。暂时性错误 - 可以进行重试。                                                                                                                       |
| `Can't find the proper key to decrypt the private key from WebauthnCredentialSpecifics.` | 存在同步的通行密钥，但设备无法解密其私钥。Google 密码管理器的同步状态不一致 - 凭证已从其他设备同步，但解密密钥不可用。开发人员无法对此进行处理。 |
| `Operation was interrupted` (原因: `The UI was interrupted - please try again.`)        | 凭证管理器 UI 被其他活动中断（来电、屏幕旋转、应用程序切换到后台）。相当于 `AbortError`。                                                                                                     |
| `Unknown credential error`                                                               | 在没有特定错误类型适用时的通用捕获。通常是瞬态错误。                                                                                                                                                                        |
| `timeout` (原因: `Canceled`)                                                            | 凭证管理器操作在用户完成生物识别验证之前超时。                                                                                                                                                       |

## 10. 将所有内容结合在一起：错误分类

下图显示了上面讨论的所有层 - 基础架构、环境、操作类型、分类和检测 - 是如何端到端连接的。这是您在设计错误跟踪时应具备的心智模型。

```mermaid
flowchart TD
    %% Global Styles
    classDef expected fill:#e3f2fd,stroke:#1565c0,stroke-width:2px;
    classDef unexpected fill:#ffebee,stroke:#c62828,stroke-width:2px;
    classDef network fill:#fff3e0,stroke:#ef6c00,stroke-dasharray: 5 5;
    classDef env fill:#f3e5f5,stroke:#7b1fa2,stroke-width:1px;
    classDef action fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;

    %% --- LEFT SIDE: NETWORK & SERVER ---
    subgraph Infrastructure ["基础结构 (服务器/网络)"]
        direction TB
        ServerErr["服务器端错误<br/>500 / 逻辑错误"]:::network
        NetErr["网络错误<br/>超时 / 400 Bad Request"]:::network

        ServerErr & NetErr -->|表现为| ClientManifest["客户端通用错误"]
    end

    %% --- CENTER: THE ENVIRONMENT STACK (From Sketch) ---
    subgraph Environment ["环境 (客户端)"]
        direction TB

        %% Layer 1: Platform Type
        subgraph Type ["第 1 层：平台"]
            Web["Web / 浏览器"]
            Native["原生 / App"]
        end

        %% Layer 2: OS Context
        subgraph OS_Layer ["第 2 层：操作系统及版本"]
            OS_Web["操作系统：Windows、macOS、Linux"]
            OS_Nat["操作系统：iOS、Android"]
            OS_Ver["操作系统版本 / 设置<br/>（屏幕锁、生物识别）"]

            Web --> OS_Web
            Native --> OS_Nat
            OS_Web & OS_Nat --> OS_Ver
        end

        %% Layer 3: Client Software
        subgraph Client_Layer ["第 3 层：客户端软件"]
            Browser["浏览器：Chrome、Safari、Edge<br/>+ 版本"]
            App["原生应用 / WebView<br/>+ 应用版本"]

            OS_Ver --> Browser
            OS_Ver --> App
        end

        %% Layer 4: WebAuthn Operation
        subgraph Op_Layer ["第 4 层：WebAuthn 操作"]
            OpType{"操作类型"}

            Login["登录<br/>(navigator.credentials.get)"]
            Reg["注册<br/>(navigator.credentials.create)"]

            Browser & App --> OpType
            OpType --> Login
            OpType --> Reg
        end

        %% Layer 5: Sub-types & Complexity
        subgraph Modality ["第 5 层：模式与复杂性"]
            Sub_CUI["CUI / 条件 UI"]
            Sub_Auto["自动操作"]
            Sub_CDA["CDA / 跨设备身份验证"]

            Login & Reg --> Sub_CUI
            Login & Reg --> Sub_Auto
            Login & Reg --> Sub_CDA
        end
    end

    %% --- BOTTOM: CLASSIFICATION & ACTION (The "What here?" section) ---
    subgraph Analysis ["错误分析与运行时"]
        direction TB

        %% Classification
        Classification{"分类"}

        Exp["预期错误<br/>(用户中止了过程)"]:::expected
        Unexp["意外错误<br/>(系统/崩溃/未知)"]:::unexpected

        ClientManifest --> Classification
        Sub_CUI & Sub_Auto & Sub_CDA --> Classification

        Classification --> Exp
        Classification --> Unexp

        %% Detection Logic
        subgraph Detection ["检测目标"]
            P1["P1：登录问题"]
            P2["P2：创建问题"]
            Baseline["检测基线漂移<br/>(预期错误增加)"]
            NewErr["检测新异常<br/>(意外错误增加)"]
        end

        Exp --> Baseline
        Unexp --> NewErr
        Exp & Unexp --> P1 & P2

        %% Actionable Outcomes
        subgraph Action ["缓解措施"]
            Fix["部署修补程序 / 新版本"]:::action
            Fallback["激活自动后备方案<br/>(禁用通行密钥)"]:::action
        end

        P1 & P2 & NewErr --> Fix
        P1 & P2 & NewErr --> Fallback
    end

    %% Connectors
    Infrastructure -.-> Environment
```

关键洞察：只有在您知道环境中的哪一层产生了它、正在运行哪项操作以及错误是预期的还是非预期的之后，原始的 `error.name` 才具有实际意义。以下部分将介绍需要记录什么以及如何对其采取操作。

## 11. 记录哪些内容以便对错误进行调试

本文中大多数错误分类都可以仅使用客户端信号来完成。仅前端可观测性 SDK 可以捕获足够的上下文以对绝大多数 WebAuthn 错误进行分类。这也是 Corbado 的可观测性 SDK 的架构方式：客户端层处理错误归因、时间、操作上下文和平台检测。服务器端日志记录添加了第二层以处理只有后端才能看到的失败。

关键要求是：每次尝试必须在端到端都能够进行连接。共享的关联 ID（例如 `auth_flow_id`）将客户端上下文与服务器验证结果连接起来。

### 11.1 客户端信号 (前端 SDK)

| **信号**                                 | **重要原因**                                                                                                                   |
| ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| `error.name` + 标准化原因存储桶    | 原始浏览器错误 + 您的分类                                                                                              |
| 操作类型                             | 条件 UI、模态登录、手动创建、条件创建、自动附加。**CDA (跨设备身份验证)** 本身就是一个复杂的系统。   |
| 完整环境上下文                   | 操作系统 + 版本，浏览器 + 版本，**硬件品牌/型号**，**身份验证器设置**（例如，是否启用了 GPM？）                           |
| 身份验证器 / 凭证管理器上下文 | 扩展及提供商损坏                                                                                              |
| 从操作开始到发生错误的时间         | 立即拒绝（`<1s`）对比用户取消（1-15s）对比超时（30s+）                                                                 |
| 网络 / 连通性状态              | 网络错误通常表现为客户端错误。追踪用户是否离线，并**将日志排入队列**，以便在连通性恢复时发送。 |
| 是否出现了 QR / 混合 UI              | 本地故障与跨设备故障                                                                                                        |
| 关联 id (`auth_flow_id`)            | 与服务器日志联接                                                                                              |

### 11.2 服务器端信号 (后端验证)

服务器验证失败发生在浏览器返回凭据和签名质询之后。这些应该是具有明确代码的结构化错误，而不是混入客户端 `DOMException` 名称的同一个桶中。参见：WebAuthn 服务器实现。

| **信号**                               | **重要原因**                                                                                                                                                                             |
| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 质询不匹配 / 质询过期   | 会话计时或重放问题                                                                                                                                                                |
| 来源 / RP ID 不匹配                  | 多域配置错误                                                                                                                                                                |
| 无效签名 / 未找到凭证 | 凭证被删除或损坏。常见情况：使用用户在服务器端已删除的通行密钥进行条件 UI 登录。使用 Signal API 保持客户端和服务器端凭证列表同步。 |
| 用户句柄不匹配                     | 账户映射问题                                                                                                                                                                         |
| 关联 id (`auth_flow_id`)          | 与客户端上下文联接                                                                                                                                                                  |

如果您想要了解完整的漏斗模型（每个步骤的流失率以及步骤之间的转化率），请参阅：使用通行密钥遥测数据以了解流失率。

[Watch on YouTube](https://www.youtube.com/watch?v=wnrXJzvBjsU)

有了这些数据后，结论就变得简单了：大多数“错误”将归结为 UX 修复、覆盖率修复或配置修复。但是，自己构建并维护这种分类是一项艰巨的持续工作。

## 12. 超越错误名称：Corbado 如何将原始错误转化为可操作的信号

上述记录清单捕获了原始信号。在大规模生产中，仅仅使用 `error.name` 是不够的。自行构建这种分类是一项持续不断的重要工作：错误消息会随着浏览器和操作系统的每次更新而变化，密码管理器提供程序会发布改变仪式行为的更新，每次功能发布都会出现新的错误特征。

### 12.1 为什么单独的 `error.name` 不够用

相同的 `NotAllowedError` 根据浏览器未为您分离的三个维度，可能意味着六种不同的事情：

| **维度**                | **浏览器为您提供的内容** | **您实际需要的内容**                                                             | **示例**                                                                                                 |
| ---------------------------- | -------------------------- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- |
| **操作上下文**        | `NotAllowedError`          | 它是条件 UI、模态登录、手动创建、条件创建还是自动附加？ | Android 对登录取消（预期的）和创建失败（意外的）返回相同的“未知错误” |
| **时间**                   | 无时长数据           | 立即（`<1s`）对比用户取消（1-15s）对比超时（30s+）                             | 200ms = 环境拒绝；5s = 用户看到对话框并取消；35s = 超时                            |
| **平台 + 身份验证器** | 通用的 `error.name`       | 每个错误的操作系统、浏览器、版本、凭证管理器                               | Chrome 上的“用户拒绝对话框”和 Safari 上的“自动填充不可用”都表面表现为 `NotAllowedError`    |

### 12.2 Corbado 的可观测性 SDK 捕获了什么

这正是 [Corbado 的可观测性 SDK](https://www.corbado.com/pricing) 旨在解决的问题。它是一个轻量级的前端集成，位于您现有的通行密钥实现之上，适用于任何 WebAuthn 服务器和任何 IDP，并在所有三个维度上自动对每一个 WebAuthn 错误进行分类：

| **功能**                       | **作用**                                                                                                                                                                                                                                                        |
| ------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **错误归因**                | 在每次执行仪式尝试时捕获操作系统、操作系统版本、浏览器、浏览器版本和身份验证器。                                                                                                                                                                         |
| **操作模式**                   | 将每个错误连接到特定的操作（条件 UI、模态登录、手动创建、条件创建、自动追加），从而使相同的 `NotAllowedError` 解析为不同的根本原因。                                                                             |
| **行动开始的时间**         | 记录从仪式启动开始的持续时间，从而无需猜测即可区分即时拒绝、用户取消和超时。                                                                                                                                               |
| **智能错误分类** | 根据完整的错误上下文（而不仅仅是名称）进行匹配，从而跨越不同的**环境**（操作系统、硬件、设置）进行规范化。对诸如 **CDA** 与本地等不同错误组进行优先级排序，并对**预期的（用户放弃）**与**非预期的（系统失败）**错误进行区分。 |
| **密码管理器碎片化**   | 检测凭证管理器扩展程序（Bitwarden、1Password、LastPass）何时拦截仪式并返回非标准的响应，从而将由扩展程序导致的失败与系统平台的失败区分开来。                                                                         |

这是 **观察** 层：能够洞察正在发生的情况，而无需修改您的实现。

### 12.3 两种调试方式：自上而下与自下而上

Corbado 的管理控制台支持两条调查路径：

**自上而下 (从仪表板探究根本原因)：**

1. **监控两种不同的异常类型：**
    - **预期的错误增加（基线偏移）：** 特定环境（如 iPhone 15 上的 iOS 18.2）是否出现了“用户取消”的情况逐渐增加？这通常表明操作系统更新引入了 UX 摩擦。
    - **未预期的错误增加（峰值）：** 完全新的错误或是失败次数的突然激增。这通常表明存在破坏性更改（内部 IDP 堆栈更新）或新浏览器版本出现了回退。
2. **按照影响划分优先级：**
    - **P1：登录问题。** 如果登录成功率下降，请立即发出警报。
    - **P2：创建问题。** 关注发展趋势，但避免因为在创建流程中出现“用户取消”高峰而叫醒工程师。
3. **深入研究环境：** 使用更细化的维度（硬件，验证设置）去找出问题究竟是全局性的，还是仅限于“装载有 Android 14 系统的三星设备”。
4. **缓解措施：** 若检测到重大的错误飙升情况，请准备好一个 **Kill Switch**。对该特定环境手动或自动禁用通行密钥，然后降级为 OTP/密码认证，以保持登录成功率并在同时开展排查。

**自下而上（错误模式到影响）：**

1. 从错误分类视图开始。检查分类错误模式及其数量。
2. 随着新模式的出现，细化错误映射（例如，新的浏览器版本发布了不同的错误消息）。
3. 错误与 KPI 更改交叉关联并作为仪表板上的注释显示，因此特定错误模式的峰值会自动链接到其影响的指标。

这两条路径殊途同归：自上而下的分析能够告诉您哪里出了问题，而自下而上的排查则能告诉您原因。[AI Analytics Assistant](https://www.corbado.com/ai-analytics-assistant) 允许您通过自然语言就错误数据和采用指标提出问题，从而将这两种分析方式连接起来。

如果团队想要对这些信号采取行动，可以转换到 **采用** 阶段，该阶段添加了[智能通行密钥](https://docs.corbado.com/corbado-connect/features/passkey-intelligence)层以自动控制仪式的进行、优化注册提示，并能够修复被破坏的通行密钥状态。对于受监管的环境或超大规模的部署，**企业版** 则增加了单租户托管、SIEM 集成以及符合 PSD2 标准的配置支持。

## 13. 结论

WebAuthn 错误名称并不是定论。它们只是提示——只有当您将它们与操作类型、时间以及平台上下文联系起来时，它们才会变得具有可操作性。

- **生产中最常见的 WebAuthn 错误名称意味着什么？** 大多数可以映射为一小部分的层次：用户控制流程 (`NotAllowedError`)，应用程序生命周期 / 并发性 (`AbortError`)，安全上下文 / 配置 (`SecurityError`)，或者是选项/状态的错误（`InvalidStateError`，`ConstraintError`，`DataError`）。占据数量绝大部分的是 `NotAllowedError`，而且其中大部分是预期的行为（用户关闭了提示框）。
- **你如何消除 `NotAllowedError` 的歧义？** 利用时序（立即拒绝还是用户取消，亦或是超时），是否有扫码 / 混合认证的标识（可用性不匹配的情况），用户激活的上下文（特别是在 iOS / Safari 上）以及操作类型（条件 UI 与模态框登录，或者通行密钥创建与条件创建相对比）。不要把所有的 `NotAllowedError` 都当做同一种失败模式来处理。
- **为什么操作类型如此重要？** 相同的 `error.name` 在条件 UI 登录的过程中出现，与在条件创建或者手动创建通行密钥的过程中（用户解散了对话框）相比，传递出的完全是一个不同的信号。将操作类型和错误一起记录下来，这正是将通用的 `NotAllowedError` 转化为可以采取措施处理的过程。
- **记录下哪些最少的上下文才能够让错误可以被调试？** 记录下 `error.name`，操作类型，从操作开始计算的故障响应时间，流程类型，是否展示过二维码/混合用户界面，操作系统 / 浏览器 / 设备信息（包含版本号），关联 id (`auth_flow_id`) 以及将服务器验证遭拒信息当成明确代码保留下来。

有两个贯穿于所有的错误类型的经验法则：永远不要向用户直接展示那些原始的浏览器错误——请始终给他们提供一条明晰的回退路径——将本地化的认证尝试与扫码 / 混合跨设备尝试区分开来，因为他们各自的故障原因是不一样的而且也需要用不同的方法来修复。
随着规模的扩大，横跨不同的浏览器、操作系统版本和凭证管理器来持续维护错误的分类是一项长期工作。与从零开始自行构建相比，可以考虑使用一种带有经过持续维护的模式库的可观测性 SDK。

## 常见问题

### 如何区分用户取消通行密钥提示与设备上没有通行密钥的情况？

在网页端，出于隐私原因，浏览器将这两种情况都合并为 `NotAllowedError`，因此您无法直接区分它们。使用时间作为辅助手段：不到 1 秒的错误通常表示环境拒绝或功能缺失，而 1-15 秒表明用户看到了对话框并关闭了它。在原生 iOS 和 Android 应用上，在请求之前设置 `preferImmediatelyAvailableCredentials` 可以在显示任何 UI 之前为您提供干净的“无凭证”信号（iOS 代码 1005，Android `GetCredentialRequest`）。

### 为什么即使通行密钥对大多数用户有效，我的 WebAuthn 错误率仍然如此之高？

在优化过的大规模通行密钥部署中，超过 95% 的记录的 WebAuthn 错误是预期的用户中止，而不是系统故障。在不区分“用户关闭提示”和真正故障的情况下，跟踪原始 `error.name` 计数会虚增错误指标，掩盖隐藏在 `NotAllowedError` 量内部的真正性能下降。按操作类型分离您的计数，并将用户取消分类桶与意外错误（如 `SecurityError`、`ConstraintError` 和 `DataError`）分开处理。

### iOS 上的 ASAuthorizationError 代码 1005 代表什么，我应该如何使用它？

iOS 代码 1005（`NotInteractive`）表示在 `ASAuthorizationController` 上设置了 `preferImmediatelyAvailableCredentials` 时，设备上没有可用的通行密钥，并且没有向用户显示任何 UI。这是一个明确的“此设备上不存在通行密钥”信号，由于隐私限制，Web 浏览器无法提供此信号。始终根据数字代码进行分类，而不是 `localizedDescription`，因为苹果的消息已本地化为 30 多种语言，并且可能随着操作系统版本的更新而改变。

### 在条件创建或自动触发的通行密钥附加流程中，我该如何处理 NotAllowedError？

在条件创建期间，`NotAllowedError`、`AbortError` 和 `InvalidStateError` 都是预期结果，应该直接忽略，而不是作为错误抛出。`NotAllowedError` 表示自动填充不可用或页面失去焦点，而 `InvalidStateError` 表示提供程序中已存在通行密钥。在尝试调用之前，使用 `getClientCapabilities()` 验证 `conditionalCreate` 支持，并检查文档可见性状态，以避免错误计数虚高。

### 我应该在 error.name 旁边记录哪些上下文，以使 WebAuthn 故障实际可调试？

捕获操作类型（条件 UI、模态登录、手动创建、条件创建或自动追加）、从操作开始的错误时间、操作系统版本、浏览器版本、硬件型号、是否出现二维码/混合 UI 以及与服务器端日志连接的关联 ID。服务器验证失败（例如挑战不匹配、无效签名和未找到凭证）应作为明确的结构化代码记录，而不是混合到与客户端 `DOMException` 名称相同的桶中。这些信号在一起可以将绝大多数错误准确无误地分类为可采取行动的桶。
