Get your free and exclusive 80-page Banking Passkey Report
passkeys e2e playwright testing webauthn virtual authenticator

通过 WebAuthn 虚拟认证器进行 Passkey 端到端 Playwright 测试

学习如何使用 WebAuthn 虚拟认证器,通过 Playwright、Nightwatch、Selenium 和 Puppeteer 为 Passkey 设置跨浏览器的端到端测试。

Blog-Post-Author

Anders

Created: June 17, 2025

Updated: June 24, 2025


我们的使命是让互联网成为一个更安全的地方,而新的登录标准 passkey 为此提供了一个卓越的解决方案。因此,我们希望帮助您更好地理解 passkey 及其特性。

1. 简介:Passkey 端到端测试#

Passkey 作为一种认证方法正被越来越广泛地接受,它依赖于 Web Authentication (WebAuthn) 作为其底层标准。由于其近期的迅速普及,相关文档和其他资源相对稀缺。这一点,加上实现 passkey 的复杂性,使得开发人员在为其平台和服务设计、实现,特别是测试 passkey 时,很难找到相关信息。

本指南旨在填补这一空白,重点关注 WebAuthn 虚拟认证器在其官方文档中未详尽涵盖的方面。例如,我们将讨论虚拟认证器的一些在文档中并非不言自明的配置选项,以及针对某些虚拟认证器未提供便捷解决方案的用例的变通方法。此外,本指南对于那些只是想寻找在测试代码中使用虚拟认证器的简单易懂示例的开发人员也很有帮助。

我们的指南使用 Playwright 的示例,提供一个简单的演练,以在您的项目中有效测试 passkey 实现。Playwright 是一个端到端 (E2E) 测试框架,它使用 Chrome DevTools Protocol (CDP) 作为浏览器自动化的协议。如果您专门寻找在 Playwright 中测试 passkey 的技术示例,可以直接跳到第 5 节。另一方面,如果您正在使用其他 E2E 测试框架,如 PuppeteerSelenium,并希望在这些框架上测试 passkey,那么测试代码的实现将与本指南中提供的示例相同或非常相似,具体取决于您使用的框架。在下一节中,我们将介绍不同 E2E 框架的背景,以及本指南对这些框架的适用性。

2. 背景:浏览器自动化和 E2E 测试框架#

2.1. 什么是浏览器自动化?#

顾名思义,浏览器自动化是指在浏览器上自动执行重复性用户操作的过程,其目的可以是为获取数据而进行网络抓取,或者在我们的案例中,是测试 Web 应用程序。WebDriver 和 Chrome DevTools Protocol (CDP) 是与本指南相关的两个主要浏览器自动化协议,因为它们都提供了 WebAuthn 虚拟认证器的实现。

2.2. 什么是 WebDriver?#

WebDriver 是一个远程控制接口,可以看作是客户端和浏览器之间通信的中间人。该协议的重点是提供一个平台和语言中立的接口,支持所有主流浏览器,包括非基于 Chromium 的浏览器,如 Firefox 和 Safari。由于 WebDriver 接口需要管理与客户端以及与浏览器的连接,这种方法以牺牲速度和稳定性为代价,换取了更广泛的浏览器支持(即更高的不稳定性)。著名的 WebDriver 客户端包括 SeleniumNightwatch

图片来源:jankaritech

2.3. 什么是 Chrome DevTools Protocol (CDP)?#

另一方面,Chrome DevTools Protocol (CDP) 在客户端和浏览器之间没有像 WebDriver 接口那样的中间人。此外,客户端和浏览器之间的通信通过套接字连接进行,这与前一种方法中客户端和 WebDriver 接口之间较慢的 HTTP 连接形成对比。这些特点使得 CDP 比 WebDriver 更快、更稳定。缺点是该协议仅支持基于 Chromium 的浏览器,如 Chrome 和 Edge。Playwright 和 Puppeteer 是使用 CDP 与浏览器通信的客户端示例。

图片来源:jankaritech

2.4. 作为基于 CDP 的 E2E 测试框架的 Puppeteer 和 Playwright#

Puppeteer 与 Playwright 类似,是一个直接构建在 CDP 之上的 E2E 框架。这意味着 Puppeteer 和 Playwright 都使用相同的 WebAuthn 虚拟认证器实现,并且通过套接字连接使用 WebAuthn 虚拟认证器的 API 通信也是相同的。

为了证明这一点,我们比较了 Playwright 和 Puppeteer 中调用 getCredentials 方法的测试代码,该方法返回到目前为止注册到虚拟认证器的所有凭证列表。我们还附加了一个简单的 credentialAdded 事件监听器,当 passkey 凭证成功注册时会触发该事件。不要被实现的细节吓到,这些细节将在后面的章节中解释。这些示例只是为了展示这两个框架之间的实现有多么相似。

Ben Gould Testimonial

Ben Gould

Head of Engineering

I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.

10,000+ devs trust Corbado & make the Internet safer with passkeys. Got questions? We've written 150+ blog posts on passkeys.

Join Passkeys Community

Playwright:

const client = await page.context().newCDPSession(page); await client.send('WebAuthn.enable'); const authenticatorId = const result = await client.send('WebAuthn.addVirtualAuthenticator', { options }); ... // 获取虚拟认证器中注册的所有凭证 const result = await client.send('WebAuthn.getCredentials', { authenticatorId }); console.log(result.credentials); // 为 credentialAdded 事件添加一个监听器,每当 passkey 凭证被注册时,就在控制台输出日志 client.on('WebAuthn.credentialAdded', () => { console.log('Credential Added!'); });

Puppeteer:

const client = await page.target().createCDPSession(); await client.send('WebAuthn.enable'); const authenticatorId = const result = await client.send('WebAuthn.addVirtualAuthenticator', { options }); ... // 获取虚拟认证器中注册的所有凭证 const result = await client.send('WebAuthn.getCredentials', { authenticatorId }); console.log(result.credentials); // 为 credentialAdded 事件添加一个监听器,每当 passkey 凭证被注册时,就在控制台输出日志 client.on('WebAuthn.credentialAdded', () => { console.log('Credential Added!'); });

虽然测试代码开头初始化 CDP 会话的方法略有不同,但在 CDP WebAuthn 虚拟认证器 API 中调用方法和处理事件是完全相同的。这意味着如果您想在 Puppeteer 中使用 WebAuthn 虚拟认证器,可以逐行遵循本指南。

Slack Icon

Become part of our Passkeys Community for updates & support.

Join

2.5. 作为基于 WebDriver 的 E2E 测试框架的 Selenium 和 Nightwatch#

SeleniumNightwatch 是依赖 WebDriver 进行浏览器自动化的 E2E 测试框架。虽然 WebDriver 的 WebAuthn 虚拟认证器实现与 CDP 的实现是分开的,但它们的 API 规范是相似的。对于 CDP WebAuthn 虚拟认证器 API 中的几乎每个方法,您都可以在 WebDriver WebAuthn 虚拟认证器 API 中找到相应的方法。然而,需要注意的一点是,虽然在 CDP WebAuthn 虚拟认证器 API 中可以为 passkey 成功添加或断言时附加事件监听器,但在 WebDriver 的对应版本中这是不可能的。

Selenium:

const driver = await new Builder().forBrowser('chrome').build(); const options = new VirtualAuthenticatorOptions(); await driver.addVirtualAuthenticator(options); ... // 获取虚拟认证器中注册的所有凭证 const credentials = await driver.getCredentials();

很明显,设置虚拟认证器实例和进行 API 调用的语法与相应的 CDP 实现不同。然而,由于两个 WebAuthn 虚拟认证器的 API 规范非常相似,因此遵循本指南在基于 WebDriver 的 E2E 测试框架上编写相应的实现是可行的。

2.6. Cypress:使用原生脚本的 E2E 测试框架#

Cypress 是一个 E2E 测试框架,它不像上面提到的框架那样主要构建在 WebDriver 或 CDP 之上。它使用原生 JavaScript 与浏览器通信。然而,它提供了对 CDP 的底层访问,这意味着可以发送原始的 CDP 命令来利用 CDP 的 WebAuthn 虚拟认证器。

Substack Icon

Subscribe to our Passkeys Substack for the latest news.

Subscribe

由于这种底层访问的语法繁琐且与上述示例大相径庭,我们不会在本指南中详细介绍。但是,关于如何在 Cypress 中调用 CDP 命令的更多信息,可以在这篇指南中找到。本指南中介绍的关于使用 CDP WebAuthn 虚拟认证器的宏观概念对于那些希望在 Cypress 上测试 passkey 的人来说仍然是相关的。

3. 是什么让使用 Playwright 进行 Passkey 端到端测试成为一个问题?#

测试 passkey 实现自然比 Web 环境中其他更简单的用户操作更具挑战性,原因有很多。处理涉及生物识别认证的动态用户交互的需求,例如指纹扫描或面部识别,增加了一层复杂性,在编写测试时可能不切实际地去详细处理。由于在认证的背景下,安全自然是一个主要问题,因此也有必要确保 passkey 认证能够无缝地集成到各种浏览器和设备中,而不留有安全漏洞的余地。

4. WebAuthn 虚拟认证器使 Passkey 端到端测试成为可能#

使用 WebAuthn 虚拟认证器可以简化处理 passkey 操作中涉及的动态用户交互的复杂性,以及测试其在不同浏览器和设备上的集成。

4.1. 什么是 WebAuthn 虚拟认证器?#

WebAuthn 虚拟认证器是 WebAuthn 标准中指定的认证器模型的软件表示。它模拟物理认证器设备的行为,例如硬件安全密钥(如 YubiKey)或生物识别扫描仪(如用于 Face ID、Touch ID 或 Windows Hello),但完全在软件内部运行(因此不涉及物理认证或生物特征扫描)。

4.2. WebAuthn 虚拟认证器有哪些好处?#

WebAuthn 虚拟认证器有两个主要好处。

4.2.1. 使用 WebAuthn 虚拟认证器进行自动化测试#

由于 WebDriver 和 CDP 是浏览器自动化工具,很明显,在这些协议中实现 WebAuthn 虚拟认证器的主要用例是自动化测试。利用这些协议,虚拟认证器能够在受控环境中(如 E2E 测试框架,例如 Playwright、Cypress、Nightwatch)对 passkey 功能进行简单而全面的测试。

4.2.2. 使用 WebAuthn 虚拟认证器进行手动测试和演示#

CDP 的 WebAuthn 虚拟认证器也可以通过 Chrome 浏览器的 DevTools 访问,并且可以用于手动测试或简单的演示目的。通过此功能,您可以在本身不支持 passkey 的设备上模拟 passkey 输入。相应地,也可以在支持 passkey 的设备上模拟一个不支持 passkey 的环境。

上面的截图显示了在 Chrome 中使用虚拟认证器进行手动测试或演示的示例。您可以看到虚拟认证器可以有不同的配置选项,并且可以跟踪凭证的添加和删除。请参阅 Google 的这篇指南,了解有关在浏览器中使用虚拟认证器的更多信息,包括配置选项和每个选项的推荐值。

4.3. WebAuthn 虚拟认证器有哪些缺点?#

虽然 WebAuthn 虚拟认证器是测试 passkey 实现的优雅解决方案,但也有一些值得注意的缺点。

4.3.1. 无法模拟特定于硬件的功能#

作为一个纯软件解决方案,WebAuthn 虚拟认证器无法复制物理认证器的独特硬件特性和安全功能。使用各种平台认证器(内置于设备中,如智能手机上的生物识别扫描仪)和各种跨平台认证器(外部设备,如硬件安全密钥)之间的区别无法使用 WebAuthn 虚拟认证器来模拟。虽然将各种类型的平台和跨平台认证器所涉及的复杂性进行黑盒简化是使用 WebAuthn 虚拟认证器的优点之一,但如果您寻求模拟和测试不同类型认证器的细微差别,则应探索其他解决方案。

4.3.2. 文档稀少和未解决的技术问题#

鉴于 WebAuthn 的采用相对较新以及 passkey 技术的新颖性,围绕虚拟认证器的生态系统仍在成熟中。这导致了全面文档的稀缺和未解决的技术挑战,特别是在将虚拟认证器与自动化测试框架集成的背景下。本指南旨在通过提供在自动化测试环境中测试 passkey 的全面见解来解决此问题,同时还专注于解决使用这些工具时仍然存在的不便之处,并为这些问题提供变通方法。

5. 如何在 Playwright 中设置 WebAuthn 虚拟认证器#

成功安装 Playwright 及其依赖项后,您可以通过创建一个以 .spec.ts 或 .test.ts 结尾的文件并包含以下内容,立即开始编写您的第一个测试:

import { test, expect } from "@playwright/test"; test("my first test", async ({ page }) => { await page.goto("https://passkeys.eu"); // 开始模拟用户操作 });

要在 Playwright 中使用 WebAuthn 虚拟认证器,只需在测试用例的开头启动一个 CDP 会话并附加一个虚拟认证器即可,如下所示:

test('signup with passkey', async ({ page }) => { // 为当前页面初始化一个 CDP 会话 const client = await page.context().newCDPSession(page); // 在此会话中启用 WebAuthn 环境 await client.send('WebAuthn.enable'); // 附加一个带有特定选项的虚拟认证器 const result = await client.send('WebAuthn.addVirtualAuthenticator', { options: { protocol: 'ctap2', transport: 'internal', hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: false, }, }); const authenticatorId = result.authenticatorId; // 模拟用户交互和断言的进一步测试步骤 ... });

配置 WebAuthn 虚拟认证器的选项:

  • protocol: 此选项指定虚拟认证器使用的协议。可能的值是 "ctap2" 和 "u2f"。
  • transport: 此选项指定虚拟认证器模拟的认证器类型。可能的值是 "usb"、"nfc"、"ble" 和 "internal"。如果设置为 "internal",它会模拟一个平台认证器,而其他值则模拟跨平台认证器。
  • hasResidentKey: 将其设置为 true 支持驻留密钥 (Resident Key)(即客户端可发现凭证)。
  • hasUserVerification: 将其设置为 true 支持用户验证。建议将此设置为 true,因为它允许模拟成功和失败的 passkey 输入。
  • isUserVerified: 将其设置为 true 模拟成功的认证场景,而 false 则模拟认证失败,例如当用户取消 passkey 输入时。请注意,此设置仅在 hasUserVerification 设置为 true 时有效。
  • automaticPresenceSimulation: 当设置为 true 时,passkey 输入会在任何认证提示出现时自动立即发生。相反,将其设置为 false 则需要在测试代码中手动启动 passkey 认证模拟。建议选择手动模拟 (false),原因有二:
    • 提高测试代码的可读性: 自动模拟可能会模糊对测试流程的理解,因为认证尝试是在测试代码中没有明确触发器的情况下模拟的。
    • 避免意外行为: 自动模拟意味着即使测试人员没有意识到 passkey 已被提示,passkey 输入也会被触发。这对于条件化 UI (Conditional UI) 尤其是一个问题,测试人员更容易忽略它。

6. WebAuthn 虚拟认证器用例#

在本节中,我们将探讨在常见和边缘用例的背景下,WebAuthn 虚拟认证器方法和事件的用法。

6.1. 如何在 Playwright 中模拟 Passkey 输入尝试#

这可能是在测试代码中使用 WebAuthn 虚拟认证器时最重要但又最令人困惑的任务,因为没有明确的内置方法来触发 passkey 输入。解决方案在于 WebAuthn 虚拟认证器的配置选项,即 isUserVerified 和 automaticPresenceSimulation。通过这些选项,我们可以通过下面描述的两种不同方法来模拟这种用户交互。

6.1.1. 方法 1:将 automaticPresenceSimulation 设置为 true 进行自动模拟#

情况 1:模拟成功的 passkey 输入

test('signup with passkey', async ({ page }) => { ... await expect(page.getByRole('heading', { level: 1 })).toHaveText('Please log in'); await page.getByRole('button', { name: 'Login' }).click(); // 成功的 passkey 输入被自动模拟 (isUserVerified=true) await expect(page.getByRole('heading', { level: 1 })).toHaveText('Welcome!'); ... });

模拟成功的 passkey 输入通常不需要在测试代码中添加额外的行。最后一行 (await expect...) 等待页面更改(由隐式成功的 passkey 输入触发)。

情况 2:模拟取消的 passkey 输入(不会触发 UI 变化)

测试失败或取消的 passkey 输入更为复杂,因为它可能不会导致 UI 的任何可观察变化。换句话说,像上一个示例中那样等待页面更改不足以确保 passkey 输入已完成处理。检查页面在隐式 passkey 输入后没有变化是无意义的,因为检查几乎肯定会在 passkey 输入完成处理之前发生。虽然虚拟认证器提供了一种通过监听事件发射来等待成功 passkey 输入被处理的方法(将在方法 2 中讨论),但目前没有内置的方法来检测失败或取消的 passkey 输入。一个变通方法是简单地添加一个硬性超时来等待 passkey 操作完成,然后再检查 UI 确实保持不变。

test('signup with passkey', async ({ page }) => { // 模拟一组用户操作以触发 passkey 提示 ... await expect(page.getByRole('heading', { level: 1 })).toHaveText('Please log in'); // 在测试中提示时模拟 passkey 输入 await inputPasskey(async () => { await page.waitForTimeout(300); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Please log in'); }); // 进一步的测试步骤 ... });

在任何一种情况下,测试代码的可读性都受到 passkey 操作隐式性的限制。如前所述,当条件化 UI (Conditional UI) 可能被提示时,也很容易忽略,在这种情况下,passkey 操作将在测试人员不知情的情况下自动完成。

6.1.2. 方法 2:将 automaticPresenceSimulation 设置为 false 进行手动模拟#

通过切换 automaticPresenceSimulation 选项的值来手动触发 passkey 输入,解决了上一种方法中遇到的问题,即在测试代码可读性方面的问题。

情况 1:模拟成功的 passkey 输入

以下代码片段模拟了成功的 passkey 输入:

async simulateSuccessfulPasskeyInput(operationTrigger: () => Promise<void>) { // 初始化事件监听器以等待成功的 passkey 输入事件 const operationCompleted = new Promise<void>(resolve => { client.on('WebAuthn.credentialAdded', () => resolve()); client.on('WebAuthn.credentialAsserted', () => resolve()); }); // 将 isUserVerified 选项设置为 true // (以便后续的 passkey 操作将成功) await client.send('WebAuthn.setUserVerified', { authenticatorId: authenticatorId, isUserVerified: true, }); // 将 automaticPresenceSimulation 选项设置为 true // (以便虚拟认证器将响应下一个 passkey 提示) await client.send('WebAuthn.setAutomaticPresenceSimulation', { authenticatorId: authenticatorId, enabled: true, }); // 执行一个触发 passkey 提示的用户操作 await operationTrigger(); // 等待接收到 passkey 已成功注册或验证的事件 await operationCompleted; // 将 automaticPresenceSimulation 选项设置回 false await client.send('WebAuthn.setAutomaticPresenceSimulation', { authenticatorId, enabled: false, }); }
test('signup with passkey', async ({ page }) => { ... // 模拟 passkey 输入,并以一个触发 passkey 提示的 promise 作为参数 await simulateSuccessfulPasskeyInput(() => page.getByRole('button', { name: 'Create account with passkeys' }).click() ); ... });

当您第一次看到这个辅助函数时,可能会觉得它很吓人。理解所有模拟 passkey 操作的技术复杂性都被抽象到这个辅助函数中会很有帮助。这意味着当它在测试代码中使用时,它使代码变得简单明了,正如上面的第二个代码片段所示。

与 6.1.1 节中的隐式方法相比,这种显式方法也提高了代码的可读性。这在提示条件化 UI (Conditional UI) 时尤其有用,因为这种显式方法可以防止在开发人员不知情的情况下无意中、隐式地完成 passkey 操作。

现在让我们来理解辅助函数的每个部分。

首先,我们定义了 operationCompleted promise,它等待 WebAuthn.credentialAdded 事件或 WebAuthn.credentialAsserted 事件,顾名思义,当 passkey 凭证被注册或验证时会分别发出。这个 promise 稍后会用到。

接下来,将 isUserVerified 选项设置为 true,以便 WebAuthn 虚拟认证器的后续 passkey 操作将成功。automaticPresenceSimulation 也设置为 true,以便 WebAuthn 虚拟认证器将响应来自网页的下一个 passkey 提示。

等待 operationTrigger promise 是为了避免竞态条件。当网页在 automaticPresenceSimulation 设置为 true 之前提示 passkey 时,就会发生竞态条件。为了防止这种情况,触发 passkey 提示的用户操作必须在 automaticPresenceSimulation 设置为 true 之后执行。在上面的示例中,用户点击名为“Create account with passkeys”的按钮来触发 passkey 提示。

用户操作完成后,我们必须等待成功的 passkey 操作完成。这是通过等待我们在辅助函数开头定义的 promise 来完成的。成功 passkey 操作的完成由 WebAuthn.credentialAdded 或 WebAuthn.credentialAsserted 事件的发射来标记。在上面的示例中,由于用户正在注册一个 passkey,将会发射 WebAuthn.credentialAdded 事件。

最后,将 automaticPresenceSimulation 选项设置回 false,以防止在测试代码的后面部分发生意外的 passkey 操作。

情况 2:模拟取消的 passkey 输入

对于取消的 passkey 输入,我们必须对上一个案例的实现进行轻微修改。在成功 passkey 输入的情况下,有事件,即 WebAuthn.credentialAdded 和 WebAuthn.credentialAsserted,在操作完成时会发出。然而,WebAuthn 虚拟认证器没有为取消或失败的 passkey 输入提供任何事件。因此,我们必须使用另一种方式来检查取消或失败的 passkey 操作是否完成。

以下代码片段模拟了失败的 passkey 输入:

async simulateFailedPasskeyInput(operationTrigger: () => Promise<void>, postOperationCheck: () => Promise<void>) { // 将 isUserVerified 选项设置为 false // (以便后续的 passkey 操作将失败) await client.send('WebAuthn.setUserVerified', { authenticatorId: authenticatorId, isUserVerified: false, }); // 将 automaticPresenceSimulation 选项设置为 true // (以便虚拟认证器将响应下一个 passkey 提示) await client.send('WebAuthn.setAutomaticPresenceSimulation', { authenticatorId: authenticatorId, enabled: true, }); // 执行一个触发 passkey 提示的用户操作 await operationTrigger(); // 等待一个预期的 UI 变化,该变化表明 passkey 操作已完成 await postOperationCheck(); // 将 automaticPresenceSimulation 选项设置回 false await client.send('WebAuthn.setAutomaticPresenceSimulation', { authenticatorId, enabled: false, }); }
test('signup with passkey', async ({ page }) => { // 模拟一组用户操作以触发 passkey 提示 ... await expect(page.getByRole('heading', { level: 1 })).toHaveText('Please log in'); // 在测试中提示时模拟 passkey 输入 await inputPasskey(async () => { await page.waitForTimeout(300); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Please log in'); }); // 进一步的测试步骤 ... });

在辅助函数中,事件监听器被一个 promise 参数 postOperationCheck 替换,该参数在 automaticPresenceSimulation 可以设置回 false 之前等待预期的 UI 变化发生。

在测试代码中,唯一的区别是必须用一个额外的 promise 来调用辅助函数,该 promise 检查预期的 UI 变化。在上面的示例中,我们检查 Web 应用程序是否已成功导航到一个页面,其中标题的文本是“Something went wrong...”。

正如在 6.1.1 节中讨论的,取消 passkey 输入可能不会导致 UI 的任何可观察变化。像该节中提供的示例一样,在这种情况下,我们必须添加一个硬性等待,然后再检查 UI 是否确实保持不变:

test('signup with passkey', async ({ page }) => { ... await expect(page.getByRole('heading', { level: 1 })).toHaveText('Please log in'); // 模拟 passkey 输入,并以一个触发 passkey 提示的 promise 作为参数 await simulateFailedPasskeyInput( () => page.getByRole('button', { name: 'Create account with passkeys' }).click(), async () => { await page.waitForTimeout(300); await expect(page.getByRole('heading', { level: 1 })).toHaveText('Please log in'); } ); ... });

6.2. 如何测试 Passkey 创建#

使用 WebAuthn 虚拟认证器的便利性在于它能够在 Web 应用程序进行 passkey 创建或删除时表现得像一个真实的认证器。测试只需执行用户操作来模拟在 Web 应用程序上创建或删除 passkey,WebAuthn 虚拟认证器会自动修改其保存的凭证信息,而无需测试代码方面做任何额外的工作。

以下是检查 Web 应用程序是否正确地向认证器注册新 passkey 的测试代码示例:

test('signup with passkey', async ({ page }) => { ... // 确认当前没有注册的凭证 const result1 = await client.send('WebAuthn.getCredentials', { authenticatorId }); expect(result1.credentials).toHaveLength(0); // 执行用户操作以模拟创建 passkey 凭证(例如,使用 passkey 输入进行用户注册) ... // 确认 passkey 已成功注册 const result2 = await client.send('WebAuthn.getCredentials', { authenticatorId }); expect(result2.credentials).toHaveLength(1); ... });

将此代码片段与 6.1 节的代码片段结合起来,我们可以在我们的演示网页上测试注册流程。以下视频是 Playwright UI 模式下测试的可视化:

6.3. 如何测试 Passkey 验证#

使用 WebAuthn 虚拟认证器验证 passkey 凭证与创建 passkey 类似,因为虚拟认证器会自动跟踪使用特定凭证执行的验证次数。

test('login with passkey', async ({ page }) => { ... // 确认只有一个凭证,并保存其 signCount const result1 = await client.send('WebAuthn.getCredentials', { authenticatorId }); expect(result1.credentials).toHaveLength(1); const signCount1 = result1.credentials[0].signCount; // 执行用户操作以模拟验证 passkey 凭证(例如,使用 passkey 输入登录) ... // 确认凭证的新 signCount 大于之前的 signCount const result2 = await client.send('WebAuthn.getCredentials', { authenticatorId }); expect(result2.credentials).toHaveLength(1); expect(result2.credentials[0].signCount).toBeGreaterThan(signCount1); ... });

以下视频演示了我们演示网页上登录流程的测试:

6.4. 如何测试 Passkey 删除#

另一方面,从 Web 应用程序中删除 passkey 不应修改 WebAuthn 虚拟认证器内的任何信息。Web 应用程序应该只能删除保存在其自己服务器中的凭证。只有用户自己才能有意识地、手动地从 WebAuthn 虚拟认证器中删除 passkey 凭证。

test('delete a registered passkey credential', async ({ page }) => { ... // 确认当前有一个注册的凭证 const result1 = await client.send('WebAuthn.getCredentials', { authenticatorId }); expect(result1.credentials).toHaveLength(1); // 执行用户操作以模拟删除 passkey 凭证 ... // 从网站删除 passkey 凭证不应从认证器中移除该凭证 const result2 = await client.send('WebAuthn.getCredentials', { authenticatorId }); expect(result2.credentials).toHaveLength(1); ... });

以下视频演示了我们演示网页上删除 passkey 凭证的测试:

6.5. 如何模拟跨设备认证#

模拟来自第二台设备(尚未注册 passkey)的跨设备认证最直观的方法是简单地通过 CDP 命令添加一个新的 WebAuthn 虚拟认证器实例,如下所示:

test('signup with passkey', async ({ page }) => { ... // 为第一台设备添加一个虚拟认证器 const authenticatorId1 = await client.send('WebAuthn.addVirtualAuthenticator', { options }); // 执行第一台设备的测试操作 ... // 为第二台设备添加一个虚拟认证器 const authenticatorId2 = await client.send('WebAuthn.addVirtualAuthenticator', { options }); // 执行第二台设备的测试操作 .. });

为了避免管理多个虚拟认证器 ID 的复杂性,也可以通过简单地从单个认证器中删除凭证,并在需要时将其添加回来,来模拟一个新设备:

test('signup with passkey', async ({ page }) => { ... const result = await client.send('WebAuthn.getCredentials', { authenticatorId }); const credential = result.credentials[0]; // 假设只有一个注册的 passkey const credentialId = credential.credentialId; await client.send('WebAuthn.removeCredential', { authenticatorId, credentialId }); // 执行没有注册 passkey 的第二台设备的测试操作 ... // 如果需要模拟拥有注册 passkey 的第一台设备,则调用此方法 await client.send('WebAuthn.addCredential', { credential }); // 执行第一台设备的测试操作 ... });

这种方法在需要模拟新设备,但旧设备不再需要使用的情况下,尤其可以简化实现。在这种情况下,您只需从虚拟认证器中清除凭证并完全丢弃其凭证即可:

test('signup with passkey', async ({ page }) => { ... const result = await client.send('WebAuthn.getCredentials', { authenticatorId }); const credential = result.credentials[0]; // 假设只有一个注册的 passkey const credentialId = credential.credentialId; await client.send('WebAuthn.clearCredentials', { authenticatorId }); // 执行没有注册 passkey 的第二台设备的测试操作 ... });

7. WebAuthn 虚拟认证器的替代方案#

探索 WebAuthn 虚拟认证器的替代方案可以在项目内部测试 passkey / WebAuthn 认证流程的方式上提供灵活性。

7.1. 使用模拟服务进行测试#

开发模拟服务或端点可以有效地模拟认证行为,通过抽象实际认证机制的复杂性来简化测试。当使用外部认证服务时,这种方法尤其有益,它使焦点能够保持在系统组件的集成和功能上,而无需深入研究认证细节。

7.2. 使用真实认证器进行集成测试#

为了对认证功能进行彻底检查,采用真实认证器进行集成测试可以提供对与硬件安全密钥(例如 YubiKeys)或生物识别设备(例如用于 Face ID、Touch ID 或 Windows Hello)交互的详细洞察。尽管由于将真实设备集成到自动化测试中的复杂性,通常是手动进行的,但开发自定义自动化脚本是可行的。这些脚本可以将真实认证器与端到端测试框架连接起来,提供更接近真实用户场景的近似值,并增强认证过程在生产环境中的可靠性。

8. 给开发者的建议#

在演示了不同选项并展示了使用 Playwright 进行 passkey / WebAuthn 端到端测试的具体代码片段之后,我们还想为刚接触该主题的开发者提供一些更普遍的建议。

8.1. 研究 E2E 测试框架的格局#

在深入测试 passkey 或任何其他认证机制之前,评估可用的 E2E 测试框架并根据您的项目需求选择最合适的选项至关重要。考虑基于 CDP 的框架(如 Playwright 和 Puppeteer)提供的速度和稳定性,与基于 WebDriver 的框架(如 Selenium 和 Nightwatch)提供的跨浏览器兼容性之间的权衡。虽然基于 CDP 的框架提供更快、更稳定的浏览器自动化,但它们仅限于基于 Chromium 的浏览器。相比之下,基于 WebDriver 的框架提供更广泛的跨浏览器兼容性,包括对非 Chromium 浏览器(如 Firefox 和 Safari)的支持,尽管性能可能较慢且不太稳定。了解这些权衡将帮助您做出明智的决定,并选择最适合您项目需求的框架。

8.2. 理解 WebAuthn 和 Passkey 背后的基本概念#

虽然 WebAuthn 虚拟认证器简化了测试 passkey 实现的过程,但开发者对 WebAuthn 标准和 passkey 背后的基本概念有扎实的理解至关重要。熟悉 WebAuthn 虚拟认证器可用的不同配置,例如 protocol、transport、hasResidentKey、hasUserVerification 和 isUserVerified。理解这些配置将使您能够微调虚拟认证器以准确模拟各种认证场景。此外,深入研究 passkey 认证的复杂性,包括其与不同浏览器和设备的集成,以及潜在的安全考虑。这些基础知识将使您能够为您的 Web 应用程序中的 passkey 认证设计全面有效的测试策略。

9. 总结#

本指南深入探讨了如何将 CDP WebAuthn 虚拟认证器与 Playwright 结合使用,重点介绍了高级概念并解决了官方文档中未涵盖的问题。我们还探讨了 Playwright 中 CDP 的替代方案以及其他 E2E 测试框架。尽管实现方式各不相同,但标准化的 WebAuthn 虚拟认证器规范确保了本指南在不同 Web 自动化协议和端到端测试框架中的适用性。要更深入地了解有关 passkey 的不同概念,请参阅我们的相关术语表,这可能有助于您根据需要微调 WebAuthn 虚拟认证器。

Add passkeys to your app in <1 hour with our UI components, SDKs & guides.

Start for free

Share this article


LinkedInTwitterFacebook

Enjoyed this read?

🤝 Join our Passkeys Community

Share passkeys implementation tips and get support to free the world from passwords.

🚀 Subscribe to Substack

Get the latest news, strategies, and insights about passkeys sent straight to your inbox.

Related Articles

Table of Contents