创建一种登录体验,既能利用通行密钥,又能适应现有的密码用户。
本指南介绍了如何使用表单自动填充功能,让用户能够同时使用通行密钥和密码进行登录。使用表单自动填充功能可打造统一的登录体验,简化从密码到更安全、更人性化的通行密钥身份验证方法的过渡。
了解如何实现 WebAuthn 的有条件界面,以便在现有登录表单中同时支持通行密钥用户和密码用户,同时尽可能减少摩擦。
为什么要使用表单自动填充功能来使用通行密钥登录?
借助通行密钥,用户可以使用指纹、人脸、设备 PIN 码登录网站。
如果所有用户都有通行密钥,身份验证流程可以是单个登录按钮。点按该按钮后,用户可以直接使用屏幕锁定功能验证账号并登录。
不过,从密码改用通行密钥存在一些挑战。在此期间,网站需要同时支持密码用户和通行密钥用户。如果要求用户记住哪些网站使用通行密钥,并要求他们预先选择登录方式,则会给用户带来糟糕的体验。
通行密钥也是一项新技术,很难清楚地加以说明。使用熟悉的自动填充界面有助于解决转换难题,并满足用户熟悉度的需求。
使用条件界面
为了同时有效支持通行密钥用户和密码用户,请在表单的自动填充建议中添加通行密钥。此方法使用 条件界面,这是 WebAuthn 标准的一项功能。
当用户将焦点置于用户名输入字段时,系统会显示一个自动填充对话框,其中会建议已存通行密钥以及已存密码。用户可以选择通行密钥或密码,然后继续登录(如果选择通行密钥,则需要使用设备屏幕锁定功能)。
这样,用户就可以使用现有登录表单登录您的网站,同时还能获享通行密钥带来的额外安全保障(前提是他们拥有通行密钥)。
通行密钥身份验证的运作方式
如需使用通行密钥进行身份验证,您可以使用 WebAuthn API。
通行密钥身份验证流程包含以下四个组成部分:
- 后端:存储用户账号详细信息,包括公钥。
- 前端:与浏览器通信并从后端提取必要数据。
- 浏览器:运行 JavaScript 并与 WebAuthn API 交互。
- 通行密钥提供程序:创建和存储通行密钥。这通常是密码管理工具(例如 Google 密码管理工具)或安全密钥。

通行密钥身份验证流程遵循以下流程:
- 用户访问登录页面,前端向后端请求身份验证质询。
- 后端会生成并返回与用户账号关联的 WebAuthn 质询。
- 前端使用质询调用
navigator.credentials.get()
,以便使用浏览器发起身份验证。 - 浏览器与通行密钥提供程序交互,提示用户选择通行密钥(通常使用通过将焦点置于登录字段触发的自动填充对话框),并使用设备屏幕锁定功能或生物识别信息验证其身份。
- 用户验证成功后,通行密钥提供程序会对质询进行签名,然后浏览器会将生成的公钥凭据(包括签名)返回给前端。
- 前端会将此凭据发送到后端。
- 后端会根据用户存储的公钥验证凭据的签名。如果验证成功,后端会为用户登录。
通过表单自动填充功能实现通行密钥身份验证
如需使用表单自动填充功能启动通行密钥身份验证,请在登录页面加载时进行条件 WebAuthn get
调用。对 navigator.credentials.get()
的此调用包含 mediation: 'conditional'
选项。
对 WebAuthn 的 navigator.credentials.get()
API 发出的条件请求不会立即显示界面。而是会在待处理状态下等待,直到用户与用户名字段的自动填充提示互动。如果用户选择通行密钥,浏览器会使用凭据解析待处理的 promise,以便用户登录,从而绕过传统的表单提交。如果用户改为选择密码,系统不会解析该 promise,并继续执行标准的密码登录流程。届时,登录用户的责任将由该页面承担。
为表单输入字段添加注释
如需启用通行密钥自动填充功能,请将 autocomplete
属性添加到表单的用户名 input
字段。将 username
和 webauthn
作为以空格分隔的值包含在内。
<input type="text" name="username" autocomplete="username webauthn" autofocus>
向此字段添加 autofocus
会在页面加载时自动触发自动填充提示,立即显示可用的密码和通行密钥。
功能检测
在调用基于条件的 WebAuthn API 调用之前,请检查以下情况:
- 浏览器支持使用
PublicKeyCredential
的 WebAuthn。
- 浏览器支持使用
PublicKeyCredential.isConditionalMediationAvailable()
的 WebAuthn 条件界面。
以下代码段展示了如何检查浏览器是否支持这些功能:
// Availability of window.PublicKeyCredential means WebAuthn is usable.
if (window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable) {
// Check if conditional mediation is available.
const isCMA = await PublicKeyCredential.isConditionalMediationAvailable();
if (isCMA) {
// Call WebAuthn authentication
}
}
从后端提取信息
您的后端需要向前端提供多个选项,以便发起 navigator.credentials.get()
调用。这些选项通常以 JSON 对象的形式从服务器上的端点提取。
options 对象中的关键属性包括:
challenge
:ArrayBuffer 中由服务器生成的质询(通常为 JSON 传输进行 Base64网址 编码)。这对于防范重放攻击至关重要。您的服务器必须针对每次登录尝试生成新的质询,并应在短时间后或尝试失败时使其失效。allowCredentials
:一组凭据描述符。传递空数组。这会提示浏览器列出指定rpId
的所有凭据。userVerification
:指定用户验证的偏好设置,例如要求设备屏幕锁定。建议的默认值为"preferred"
。可能的值包括:"required"
:用户验证必须由身份验证器(例如 PIN 码或生物识别)执行。如果无法执行验证,则操作会失败。"preferred"
:身份验证器会尝试进行用户验证,但即使不进行验证,操作也能成功。"discouraged"
:身份验证器应尽可能避免用户验证。
rpId
:您的依赖方 ID,通常是您网站的域名(例如example.com
)。此值必须与创建通行密钥凭据时使用的rp.id
完全匹配。
您的服务器应构建此 options 对象。ArrayBuffer
值(例如 challenge
)必须采用 Base64网址 编码才能进行 JSON 传输。在前端,解析 JSON 后,使用 PublicKeyCredential.parseRequestOptionsFromJSON()
将对象(包括解码 Base64网址 字符串)转换为 navigator.credentials.get()
期望的格式。
以下代码段展示了如何提取和解码使用通行密钥进行身份验证所需的信息。
// Fetch an encoded PubicKeyCredentialRequestOptions from the server.
const _options = await fetch('/webauthn/signinRequest');
// Deserialize and decode the PublicKeyCredentialRequestOptions.
const decoded_options = JSON.parse(_options);
const options = PublicKeyCredential.parseRequestOptionsFromJSON(decoded_options);
...
使用 conditional
标志调用 WebAuthn API 以对用户进行身份验证
准备好 publicKeyCredentialRequestOptions
对象(在以下示例代码中称为 options
)后,调用 navigator.credentials.get()
以启动基于条件的通行密钥身份验证。
// To abort a WebAuthn call, instantiate an AbortController.
const abortController = new AbortController();
// Invoke WebAuthn to authenticate with a passkey.
const credential = await navigator.credentials.get({
publicKey: options,
signal: abortController.signal,
// Specify 'conditional' to activate conditional UI
mediation: 'conditional'
});
此调用的关键参数:
publicKey
:这必须是您在上一步中从服务器提取并处理的publicKeyCredentialRequestOptions
对象(在示例中名为options
)。signal
:传递AbortController
的信号(例如abortController.signal
)可让您以编程方式取消get()
请求。如果您想调用其他 WebAuthn 调用,这会非常有用。mediation: 'conditional'
:这是使 WebAuthn 调用具有条件性的关键标志。它会告知浏览器等待用户与自动填充提示互动,而不是立即显示模态对话框。
将返回的公钥凭据发送到 RP 服务器
如果用户选择通行密钥并成功验证自己的身份(例如,使用设备屏幕锁定功能),navigator.credentials.get()
承诺便会解析。这会将 PublicKeyCredential
对象返回给您的前端。
有多个原因可能会导致 promise 被拒绝。您应通过检查 Error
对象的 name
属性,在代码中处理这些错误:
NotAllowedError
:用户取消了操作,或者未选择通行密钥。AbortError
:操作已被中止,可能是您的代码使用AbortController
导致的。- 其他异常:发生意外错误。浏览器通常会向用户显示错误对话框。
PublicKeyCredential
对象包含多个属性。与身份验证相关的关键属性包括:
id
:经过身份验证的通行密钥凭据的 base64url 编码 ID。rawId
:凭据 ID 的 ArrayBuffer 版本。response.clientDataJSON
:客户端数据的 ArrayBuffer。此字段包含质询以及您的服务器必须验证的来源等信息。response.authenticatorData
:身份验证器数据的 ArrayBuffer。此字段包含 RP ID 等信息。response.signature
:包含签名的 ArrayBuffer。此值是凭据的核心,您的服务器必须使用存储的凭据公钥验证此签名。response.userHandle
:一个 ArrayBuffer,其中包含在通行密钥注册期间提供的用户 ID。authenticatorAttachment
:指示身份验证器是客户端设备的一部分 (platform
) 还是外部 (cross-platform
)。如果 用户使用手机登录,则可能会发生cross-platform
附加。在这种情况下,不妨提示用户在当前设备上创建通行密钥,以备日后使用。type
:此字段始终设置为"public-key"
。
如需将此 PublicKeyCredential
对象发送到后端,请先调用 .toJSON()
方法。此方法会创建可序列化为 JSON 的凭据版本,该版本会正确处理将 ArrayBuffer
属性(例如 rawId
、clientDataJSON
、authenticatorData
、signature
和 userHandle
)转换为 Base64网址 编码字符串。然后,使用 JSON.stringify()
将此对象转换为字符串,并将其发送到服务器的请求正文中。
...
// Encode and serialize the PublicKeyCredential.
const _result = credential.toJSON();
const result = JSON.stringify(_result);
// Encode and send the credential to the server for verification.
const response = await fetch('/webauthn/signinResponse', {
method: 'post',
credentials: 'same-origin',
body: result
});
验证签名
当后端服务器收到公钥凭据时,必须验证其真实性。这些行为包括:
- 解析凭据数据。
- 查找与凭据的
id
关联的存储公钥。 - 使用存储的公钥验证收到的
signature
。 - 验证其他数据,例如质询和来源。
我们建议使用服务器端 FIDO/WebAuthn 库来安全地处理这些加密操作。您可以在 awesome-webauthn GitHub 代码库中找到开源库。
如果签名和所有其他断言均有效,服务器就可以为用户登录。如需详细了解服务器端验证步骤,请参阅服务器端通行密钥身份验证
如果在后端上找不到匹配的凭据,则发出信号
如果您的后端服务器在登录期间找不到具有匹配 ID 的凭据,则表示用户可能之前已从您的服务器中删除此通行密钥,但并未从其通行密钥提供程序中删除。如果通行密钥提供程序继续建议不再适用于您的网站的通行密钥,这种不匹配可能会导致用户体验不佳。为了改进这一点,您应向通行密钥提供程序发出信号,以移除孤岛通行密钥。
您可以使用 Webauthn Signal API 中的 PublicKeyCredential.signalUnknownCredential()
方法告知通行密钥提供程序指定凭据已被移除或不存在。如果您的服务器指明(例如,使用特定 HTTP 状态代码 404)所提供的凭据 ID 未知,请在客户端调用此静态方法。向此方法提供 RP ID 和未知凭据 ID。通行密钥提供程序(如果支持该信号)应移除通行密钥。
// Detect authentication failure due to lack of the credential
if (response.status === 404) {
// Feature detection
if (PublicKeyCredential.signalUnknownCredential) {
await PublicKeyCredential.signalUnknownCredential({
rpId: "example.com",
credentialId: "vI0qOggiE3OT01ZRWBYz5l4MEgU0c7PmAA" // base64url encoded credential ID
});
} else {
// Encourage the user to delete the passkey from the password manager nevertheless.
...
}
}
身份验证后
根据用户登录的方式,我们建议采用不同的流程。
如果用户未使用通行密钥登录
如果用户未使用通行密钥登录您的网站,则可能未为该账号注册通行密钥,或者其当前设备上没有通行密钥。这是一个鼓励用户创建通行密钥的好时机。请考虑以下方法:
- 将密码升级为通行密钥:使用有条件创建(这项 WebAuthn 功能可让浏览器在用户成功使用密码登录后自动为其创建通行密钥)。这可以简化创建过程,从而显著提高通行密钥的采用率。如需了解其运作方式和实现方法,请参阅帮助用户更顺畅地采用通行密钥
- 手动提示创建通行密钥:鼓励用户创建通行密钥。在用户完成更复杂的登录流程(例如多重身份验证 [MFA])后,此方法可能会有效。不过,请避免过多提示,因为这可能会干扰用户体验。”
如需了解如何鼓励用户创建通行密钥以及其他良好做法,请参阅向用户传达通行密钥中的示例。
如果用户已使用通行密钥登录
用户使用通行密钥成功登录后,您有几种机会进一步提升用户体验并保持账号一致性。
鼓励用户在跨设备身份验证后创建新的通行密钥
如果用户使用跨设备机制(例如,使用手机扫描二维码)通过通行密钥登录,则他们使用的通行密钥可能不会存储在他们登录的设备本地。以下情况会导致这一结果:
- 用户拥有通行密钥,但通行密钥提供程序不支持登录操作系统或浏览器。
- 用户已失去对登录设备上的通行密钥提供程序的访问权限,但通行密钥仍可在其他设备上使用。
在这种情况下,不妨考虑提示用户在当前设备上创建新的通行密钥。这样一来,他们日后便无需重复跨设备登录流程。如需确定用户是否使用跨设备通行密钥登录,请检查凭据的 authenticatorAttachment
属性。如果其值为 "cross-platform"
,则表示跨设备身份验证。如果是,请说明创建新通行密钥的便捷性,并引导他们完成创建过程。
使用信号将通行密钥详细信息与提供方同步
为确保一致性和更好的用户体验,依赖方 (RP) 可以使用 WebAuthn Signals API 向通行密钥提供程序传达有关凭据和用户信息的更新。
例如,为了确保通行密钥提供程序的用户通行密钥列表准确无误,请让后端的凭据保持同步。您可以发出通行密钥已不存在的信号,以便通行密钥提供方移除不必要的通行密钥。
同样,您可以指明用户是否更新了您服务中的用户名或显示名称,以帮助确保通行密钥提供程序显示的用户信息(例如,在账号选择对话框中)保持最新状态。
如需详细了解确保通行密钥保持一致的最佳实践,请参阅使用 Signal API 确保通行密钥与服务器上的凭据保持一致。
不要求提供第二重身份验证
通行密钥提供强大的内置保护机制,可防范钓鱼式攻击等常见威胁。 因此,第二个身份验证因素不会显著提高安全性。而是会在用户登录时为其创建不必要的步骤。
核对清单
- 允许用户通过表单自动填充功能使用通行密钥登录。
- 当后端找不到通行密钥的匹配凭据时发出信号。
- 如果用户在登录后尚未创建通行密钥,请提示用户手动创建通行密钥。
- 在用户使用密码(和第二种因素)登录后自动创建通行密钥(有条件创建)。
- 如果用户使用跨设备通行密钥登录,系统会提示创建本地通行密钥。
- 在登录后或发生更改时,向提供方发送可用通行密钥列表和更新后的用户详细信息(用户名、显示名称)。