创建用于无密码登录的通行密钥

通行密钥使您的用户帐户更安全、更简单、更易于使用。

使用 通行密钥 可以增强安全性、简化登录并取代密码。与常规密码(用户必须记住并手动输入)不同,通行密钥使用设备的屏幕锁定机制(如生物识别或 PIN 码),并降低了网络钓鱼风险和凭据盗窃风险。

通行密钥使用 Google 密码管理器和 iCloud 钥匙串等通行密钥提供商在设备之间同步。

必须创建通行密钥,将私钥安全地存储到通行密钥提供商,同时将必要的元数据及其公钥存储在您的服务器上以进行身份验证。私钥在用户在有效域上验证后发出签名,使通行密钥具有防网络钓鱼能力。公钥验证签名,而无需存储敏感凭据,从而使通行密钥能够抵御凭据盗窃。

创建通行密钥的工作原理

在用户可以使用通行密钥登录之前,您应该创建通行密钥,将其与用户帐户关联,并将公钥存储在您的服务器上。

您可以要求用户在以下情况之一中创建通行密钥

  • 注册期间或注册后。
  • 登录后。
  • 从另一台设备使用通行密钥登录后(即 [authenticatorAttachment](https://webdev.ac.cn/articles/passkey-form-autofill#authenticator-attachment)cross-platform)。
  • 在用户可以管理其通行密钥的专用页面上。

要创建通行密钥,您可以使用 WebAuthn API

通行密钥注册流程的四个组成部分是

  • 后端:存储用户帐户详细信息,包括公钥。
  • 前端:与浏览器通信并从后端获取必要的数据。
  • 浏览器:运行您的 JavaScript 并与 WebAuthn API 交互。
  • 通行密钥提供商:创建和存储通行密钥。这通常是密码管理器(如 Google 密码管理器)或安全密钥。
The process of creating and registering a passkey
创建和注册通行密钥的过程。

在创建通行密钥之前,请确保系统满足以下先决条件

  • 用户帐户在有意义的短时间内通过安全方法(例如,电子邮件、电话验证或身份联合)进行验证。

  • 前端和后端可以安全地通信以交换凭据数据。

  • 浏览器支持 WebAuthn 和通行密钥创建。

我们将在以下部分向您展示如何检查其中的大多数。

一旦系统满足这些条件,就会发生以下过程来创建通行密钥

  1. 当用户启动操作时(例如,单击其通行密钥管理页面中的“创建通行密钥”按钮或完成注册后),系统会触发通行密钥创建过程。
  2. 前端从后端请求必要的凭据数据,包括用户信息、质询和凭据 ID 以防止重复。
  3. 前端调用 navigator.credentials.create() 以提示设备的通行密钥提供商使用来自后端的信息生成通行密钥。请注意,此调用返回一个 promise。
  4. 用户的设备使用生物识别方法、PIN 码或图案对用户进行身份验证以创建通行密钥。
  5. 通行密钥提供商创建通行密钥并将公钥凭据返回给前端,从而解析 promise。
  6. 前端将生成的公钥凭据发送到后端。
  7. 后端存储公钥和其他重要数据以供将来身份验证,
  8. 后端通知用户(例如,使用电子邮件)以确认通行密钥创建并检测潜在的未授权访问。

此过程确保为用户提供安全无缝的通行密钥注册过程。

兼容性

大多数浏览器都支持 WebAuthn,但存在一些小的差距。有关浏览器和操作系统兼容性详细信息,请参阅 passkeys.dev

创建新的通行密钥

要创建新的通行密钥,前端应遵循以下过程

  1. 检查兼容性。
  2. 从后端获取信息。
  3. 调用 WebAuth API 以创建通行密钥。
  4. 将返回的公钥发送到后端。
  5. 保存凭据。

以下部分展示了如何执行此操作。

检查兼容性

在显示“创建新的通行密钥”按钮之前,前端应检查是否

  • 浏览器支持带有 PublicKeyCredential 的 WebAuthn。

浏览器支持

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

来源

  • 设备支持带有 PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable() 的平台验证器(可以创建通行密钥并使用通行密钥进行身份验证)。

浏览器支持

  • Chrome: 67.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

来源

  • 浏览器支持带有 PublicKeyCredenital.isConditionalMediationAvailable()WebAuthn 条件式 UI

浏览器支持

  • Chrome: 108.
  • Edge: 108.
  • Firefox: 119.
  • Safari: 16.

来源

以下代码段展示了如何在显示与通行密钥相关的选项之前检查兼容性。

// Availability of `window.PublicKeyCredential` means WebAuthn is usable.  
// `isUserVerifyingPlatformAuthenticatorAvailable` means the feature detection is usable.  
// `isConditionalMediationAvailable` means the feature detection is usable.  
if (window.PublicKeyCredential &&  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&  
    PublicKeyCredential.isConditionalMediationAvailable) {  
  // Check if user verifying platform authenticator is available.  
  Promise.all([  
    PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),  
    PublicKeyCredential.isConditionalMediationAvailable(),  
  ]).then(results => {  
    if (results.every(r => r === true)) {  
      // Display "Create a new passkey" button  
    }  
  });  
}  

在此示例中,只有在满足所有条件时才应显示创建新的通行密钥按钮。

从后端获取信息

当用户单击该按钮时,从后端获取调用 navigator.credentials.create() 所需的信息。

以下代码段展示了一个 JSON 对象,其中包含调用 navigator.credentials.create() 所需的信息

// Example `PublicKeyCredentialCreationOptions` contents
{
  challenge: *****,
  rp: {
    name: "Example",
    id: "example.com",
  },
  user: {
    id: *****,
    name: "john78",
    displayName: "John",
  },
  pubKeyCredParams: [{
    alg: -7, type: "public-key"
  },{
    alg: -257, type: "public-key"
  }],
  excludeCredentials: [{
    id: *****,
    type: 'public-key',
    transports: ['internal'],
  }],
  authenticatorSelection: {
    authenticatorAttachment: "platform",
    requireResidentKey: true,
  }
}

对象中的键值对包含以下信息

  • challenge:服务器生成的 ArrayBuffer 格式的质询,用于此注册。
  • rp.id:RP ID(信赖方 ID),域和网站可以指定其域或可注册后缀。例如,如果 RP 的来源是 https://login.example.com:1337,则 RP ID 可以是 login.example.comexample.com。如果 RP ID 指定为 example.com,则用户可以在 login.example.comexample.com 上的任何子域上进行身份验证。有关此方面的更多信息,请参阅 通过关联来源请求允许跨站点重复使用通行密钥
  • rp.name:RP(信赖方)的名称。这在 WebAuthn L3 中已弃用,但包含在内是为了兼容性原因。
  • user.id:ArrayBuffer 格式的唯一用户 ID,在帐户创建时生成。它应该是永久性的,与可能可编辑的用户名不同。用户 ID 标识一个帐户,但 不应包含任何个人身份信息 (PII)。您的系统中可能已经有一个用户 ID,但如果需要,请专门为通行密钥创建一个用户 ID,以使其不包含任何 PII。
  • user.name:用户将识别的帐户的唯一标识符,例如他们的电子邮件地址或用户名。这将显示在帐户选择器中。
  • user.displayName:帐户的必需的、更方便用户使用的名称。它不需要是唯一的,可以是用户选择的名称。如果您的站点没有合适的包含在此处的值,请传递一个空字符串。这可能会显示在帐户选择器上,具体取决于浏览器。
  • pubKeyCredParams:指定 RP(信赖方)支持的公钥算法。我们建议将其设置为 [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]。这指定了对使用 P-256 的 ECDSA 和 RSA PKCS#1 的支持,并且支持这些算法可以提供完整的覆盖范围。
  • excludeCredentials:已注册凭据 ID 的列表。通过提供已注册凭据 ID 的列表,防止同一设备注册两次。如果提供了 transports 成员,则应包含在每个凭据注册期间调用 getTransports() 的结果。
  • authenticatorSelection.authenticatorAttachment:如果此通行密钥创建是从密码升级(例如,在登录后的促销活动中),则将其设置为 "platform" 以及 hint: ['client-device']"platform" 表示 RP 想要平台验证器(嵌入到平台设备的验证器),该验证器不会提示插入 USB 安全密钥等。用户有一个更简单的选项来创建通行密钥。
  • authenticatorSelection.requireResidentKey:将其设置为布尔值 true可发现凭据(常驻密钥) 将用户信息存储到通行密钥,并允许用户在身份验证时选择帐户。
  • authenticatorSelection.userVerification:指示是否"required""preferred""discouraged" 使用设备屏幕锁进行用户验证。默认值为 "preferred",这意味着验证器可能会跳过用户验证。将其设置为 "preferred" 或省略该属性。

我们建议在服务器上构造对象,使用 Base64URL 对 ArrayBuffer 进行编码,然后从前端获取它。这样,您可以使用 PublicKeyCredential.parseCreationOptionsFromJSON() 解码有效负载,并将其直接传递给 navigator.credentials.create()

以下代码段展示了如何获取和解码创建通行密钥所需的信息。

// Fetch an encoded `PubicKeyCredentialCreationOptions` from the server.
const _options = await fetch('/webauthn/registerRequest');

// Deserialize and decode the `PublicKeyCredentialCreationOptions`.
const decoded_options = JSON.parse(_options);
const options = PublicKeyCredential.parseCreationOptionsFromJSON(decoded_options);
...

调用 WebAuthn API 以创建通行密钥

调用 navigator.credentials.create() 以创建新的通行密钥。API 返回一个 promise,等待用户的交互,显示一个模式对话框。

浏览器支持

  • Chrome: 60.
  • Edge: 18.
  • Firefox: 60.
  • Safari: 13.

来源

// Invoke WebAuthn to create a passkey.
const credential = await navigator.credentials.create({
  publicKey: options
});

将返回的公钥凭据发送到后端

在使用设备的屏幕锁验证用户后,将创建一个通行密钥,并且 promise 将被解析,从而将 PublicKeyCredential 对象返回到前端。

promise 可能会因各种原因而被拒绝。您可以通过检查 Error 对象的 name 属性来处理这些错误

  • InvalidStateError:设备上已存在通行密钥。不会向用户显示错误对话框。站点不应将其视为错误。用户希望注册本地设备,并且已注册。
  • NotAllowedError:用户已取消操作。
  • AbortError:操作已中止。
  • 其他异常:发生了一些意外情况。浏览器向用户显示一个错误对话框。

公钥凭据对象包含以下属性

  • id:创建的通行密钥的 Base64URL 编码 ID。此 ID 帮助浏览器确定在身份验证时设备中是否存在匹配的通行密钥。此值必须存储在后端的数据库中。
  • rawId:凭据 ID 的 ArrayBuffer 版本。
  • response.clientDataJSON:ArrayBuffer 编码的客户端数据。
  • response.attestationObject:ArrayBuffer 编码的证明对象。其中包含重要信息,例如 RP ID、标志和公钥。
  • authenticatorAttachment:当在此支持通行密钥的设备上创建凭据时,返回 "platform"
  • type:此字段始终设置为 "public-key"

使用 .toJSON() 方法对对象进行编码,使用 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/registerResponse', {
  method: 'post',
  credentials: 'same-origin',
  body: result
});
...

保存凭据

在后端接收到公钥凭据后,我们建议使用服务器端库或解决方案,而不是编写自己的代码来处理公钥凭据。

然后,您可以将从凭据检索到的信息存储到数据库中以供将来使用。

以下列表包含建议保存的属性

  • 凭据 ID:与公钥凭据一起返回的凭据 ID。
  • 凭据名称:凭据的名称。根据 创建它的通行密钥提供商命名它,该提供商可以根据 AAGUID 识别
  • 用户 ID:用于创建通行密钥的用户 ID。
  • 公钥:与公钥凭据一起返回的公钥。这是验证通行密钥断言所必需的。
  • 创建日期和时间:记录通行密钥的创建日期和时间。这对于识别通行密钥很有用。
  • 上次使用日期和时间:记录用户上次使用通行密钥登录的日期和时间。这对于确定用户使用了哪个通行密钥(或未使用哪个通行密钥)很有用。
  • AAGUID:通行密钥提供商的唯一标识符。
  • 备份资格标志:如果设备符合通行密钥同步的条件,则为 true。此信息可帮助用户在通行密钥管理页面上识别可同步的通行密钥和设备绑定的(不可同步的)通行密钥。

请按照 服务器端通行密钥注册 中的更详细说明进行操作

在注册失败时发出信号

如果通行密钥注册失败,可能会给用户带来困惑。如果通行密钥提供商中存在通行密钥并且用户可以使用,但关联的公钥未存储到服务器端,则使用通行密钥进行的登录尝试将永远不会成功,并且很难进行故障排除。请务必告知用户是否是这种情况。

为了防止出现这种情况,您可以使用 Signal API 向通行密钥提供商发出未知通行密钥信号。通过使用 RP ID 和凭据 ID 调用 PublicKeyCredential.signalUnknownCredential(),RP 可以通知通行密钥提供商指定的凭据已被删除或不存在。这取决于通行密钥提供商如何处理此信号,但如果支持,则应删除关联的通行密钥。

// 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.
    ...
  }
}

要了解有关 Signal API 的更多信息,请阅读 使用 Signal API 使通行密钥与服务器上的凭据保持一致

向用户发送通知

在注册通行密钥时发送通知(例如电子邮件)可帮助用户检测未经授权的帐户访问。如果攻击者在用户不知情的情况下创建了通行密钥,则即使在密码更改后,该通行密钥仍然可用于将来的滥用。通知会提醒用户并有助于防止这种情况发生。

清单

  • 在允许用户创建通行密钥之前,请验证用户身份(最好使用电子邮件或安全方法)。
  • 使用 excludeCredentials 防止为同一通行密钥提供商创建重复的通行密钥。
  • 保存 AAGUID 以识别通行密钥提供商并为用户命名凭据。
  • 如果注册通行密钥的尝试失败,请使用 PublicKeyCredential.signalUnknownCredential() 发出信号。
  • 在为用户的帐户创建和注册通行密钥后,向用户发送通知。

资源

下一步:通过表单自动填充使用通行密钥登录