创建利用通行密钥的登录流程,同时仍能满足现有密码用户的需求。
本指南介绍了如何使用表单自动填充功能,让用户能够使用通行密钥和密码登录。使用表单自动填充功能可打造统一的登录体验,从而简化从密码到更安全、更人性化的通行密钥身份验证方法的过渡。
了解如何实现 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.getClientCapabilities()进行功能检测。
- 浏览器支持使用
conditionalGet的 WebAuthn 条件式界面。
以下代码段展示了如何检查浏览器是否支持这些功能:
if (window.PublicKeyCredential && PublicKeyCredential.getClientCapabilities) {
const capabilities = await PublicKeyCredential.getClientCapabilities();
// Check if conditional mediation is available.
if (capabilities.conditionalGet === true) {
// The browser supports conditional mediation.
}
}
从后端提取信息
您的后端需要向前端提供多个选项,以发起 navigator.credentials.get() 调用。这些选项通常以 JSON 对象的形式从服务器上的端点提取。
options 对象中的关键属性包括:
challenge:ArrayBuffer 格式的服务器生成的质询(通常采用 Base64网址 编码,以便通过 JSON 传输)。这对于防范重放攻击至关重要。您的服务器必须为每次登录尝试生成新的质询,并且应在短时间后或尝试失败时使该质询失效。allowCredentials:凭据描述符数组。传递一个空数组。这会促使浏览器列出指定rpId的所有凭据。userVerification:指定您对用户验证的偏好设置,例如要求使用设备屏幕锁定功能。默认值和建议值为"preferred"。可能的值包括:"required":用户验证必须由身份验证器执行(例如 PIN 码或生物识别)。如果无法执行验证,则操作失败。"preferred":验证器尝试进行用户验证,但即使不进行用户验证,操作也可以成功。"discouraged":身份验证器应尽可能避免用户验证。
rpId:信赖方 ID,通常是您网站的网域(例如example.com)。此值必须与创建通行密钥凭据时使用的rp.id完全一致。
您的服务器应构建此选项对象。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() promise 会解析。此方法会向您的前端返回一个 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:包含在通行密钥注册期间提供的用户 ID 的 ArrayBuffer。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.
...
}
}
通过身份验证后
我们会根据用户的登录方式建议不同的流程。
如果用户已登录,但未使用通行密钥
如果用户在未通过通行密钥登录您网站的情况下登录了账号,则可能是因为该账号或当前设备上未注册通行密钥。这是鼓励用户创建通行密钥的好时机。不妨考虑以下方法:
- 将密码升级为通行密钥:使用 conditional create(一种 WebAuthn 功能),让浏览器在用户成功使用密码登录后自动为用户创建通行密钥。这可以简化通行密钥创建流程,从而显著提高通行密钥的采用率。了解其运作方式以及如何在帮助用户更顺畅地采用通行密钥中实现它
- 手动提示创建通行密钥:鼓励用户创建通行密钥。在用户完成更复杂的登录流程(例如多重身份验证 [MFA])后,此方法会非常有效。不过,请避免过度提示,以免干扰用户体验。”
如需了解如何鼓励用户创建通行密钥并学习其他最佳实践,请参阅向用户传达通行密钥相关信息中的示例。
如果用户已使用通行密钥登录
用户成功使用通行密钥登录后,您可以通过多种方式进一步提升用户体验并保持账号一致性。
鼓励用户在跨设备身份验证后创建新的通行密钥
如果用户使用跨设备机制(例如,使用手机扫描二维码)通过通行密钥登录,那么他们使用的通行密钥可能不会存储在他们登录的设备上。以下情况会导致这一结果:
- 他们拥有通行密钥,但通行密钥提供商不支持登录操作系统或浏览器。
- 他们无法再访问登录设备上的通行密钥提供程序,但另一部设备上仍有可用的通行密钥。
在这种情况下,请考虑提示用户在当前设备上创建新的通行密钥。这样一来,他们日后便无需重复执行跨设备登录流程。如需确定用户是否已使用跨设备通行密钥登录,请检查凭据的 authenticatorAttachment 属性。如果其值为 "cross-platform",则表示跨设备身份验证。如果用户同意,请说明创建新通行密钥的便利性,并引导他们完成创建过程。
使用信号与提供方同步通行密钥详细信息
为确保一致性和更好的用户体验,信赖方 (RP) 可以使用 WebAuthn Signals API 将凭据和用户信息方面的更新传达给通行密钥提供方。
例如,为了确保通行密钥提供程序的用户通行密钥列表准确无误,请使后端凭据保持同步。您可以发出通行密钥不再存在的信号,以便通行密钥提供方移除不必要的通行密钥。
同样,您也可以在用户更新其在您服务中的用户名或显示名称时发出信号,以帮助确保通行密钥提供方(例如在账号选择对话框中)显示的用户信息是最新的。
如需详细了解如何确保通行密钥保持一致,请参阅使用 Signal API 确保通行密钥与服务器上的凭据保持一致。
不要求进行第二重身份验证
通行密钥提供强大的内置保护机制,可防范钓鱼式攻击等常见威胁。因此,第二重身份验证因素并不能显著提高安全性。反而会在登录过程中为用户增加不必要的步骤。
核对清单
- 允许用户通过表单自动填充功能使用通行密钥登录。
- 在后端找不到通行密钥的匹配凭据时发出信号。
- 如果用户在登录后尚未创建通行密钥,则提示用户手动创建通行密钥。
- 在用户使用密码(和第二重身份验证)登录后,自动创建通行密钥(有条件创建)。
- 如果用户已使用跨设备通行密钥登录,则提示用户创建本地通行密钥。
- 在登录后或发生更改时,向提供方发送可用通行密钥列表和更新后的用户详细信息(用户名、显示名称)信号。