创建通行密钥以实现无密码登录

通行密钥可提高用户账号的安全性,并简化用户账号的管理和使用。

发布时间:2022 年 10 月 12 日;最后更新时间:2026 年 4 月 9 日

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

通行密钥会通过 Google 密码管理工具和 iCloud 钥匙串等通行密钥提供方在设备之间同步。

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

创建通行密钥的运作方式

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

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

  • 注册期间或之后。
  • 登录后。
  • 使用另一部设备中的通行密钥(即 authenticatorAttachmentcross-platform)登录后。
  • 在专用页面上,用户可以管理自己的通行密钥。

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

通行密钥注册流程包含以下四个组成部分:

  • 后端:存储用户账号详细信息,包括公钥。
  • 前端:与浏览器通信,并从后端提取必要的数据。
  • 浏览器:运行 JavaScript 并与 WebAuthn API 互动。
  • 通行密钥提供方:创建并存储通行密钥。这通常是密码管理工具(例如 Google 密码管理工具)或安全密钥。
创建和注册通行密钥的过程
创建和注册通行密钥的过程。

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

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

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

  • 浏览器支持 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。

Browser Support

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

Source

  • 浏览器支持使用 PublicKeyCredential.getClientCapabilities() 进行功能检测。

Browser Support

  • Chrome: 133.
  • Edge: 133.
  • Firefox: 135.
  • Safari: 17.4.

Source

  • 浏览器支持使用 conditionalGetWebAuthn 条件式界面

  • 设备支持具有 passkeyPlatformAuthenticator 的平台身份验证器(可在设备上创建通行密钥并进行身份验证)。

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

if (window.PublicKeyCredential && PublicKeyCredential.getClientCapabilities) {
  const capabilities = await PublicKeyCredential.getClientCapabilities();
  if (capabilities.conditionalGet === true &&
      capabilities.passkeyPlatformAuthenticator === true) {
    // The browser supports passkeys and the conditional UI.
  }
}

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

从后端提取信息

当用户点击该按钮时,从后端提取所需信息以调用 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.idRP ID(信赖方 ID),网域和网站可以指定其网域或可注册的后缀。例如,如果 RP 的来源为 https://login.example.com:1337,则 RP ID 可以是 login.example.comexample.com。如果将 RP ID 指定为 example.com,则用户可以在 login.example.com 上或 example.com 上的任何子网域上进行身份验证。如需详细了解此功能,请参阅通过相关来源请求允许在您的网站上重复使用通行密钥
  • rp.name:信赖方 (RP) 的名称。此属性在 WebAuthn L3 中已弃用,但出于兼容性考虑而包含在内。
  • user.id:以 ArrayBuffer 格式表示的唯一用户 ID,在创建账号时生成。它应该是永久性的,不像用户名那样可以修改。用户 ID 用于标识账号,但不得包含任何个人身份信息 (PII)。您可能已在系统中拥有用户 ID,但如果需要,请专门为通行密钥创建一个用户 ID,以确保其中不包含任何 PII。
  • user.name:用户可识别的账号唯一标识符,例如电子邮件地址或用户名。此 ID 将显示在账号选择器中。
  • user.displayName:必需,更方便用户记忆的账号名称。该名称不必是唯一的,可以是用户选择的名称。如果您的网站没有适合此参数的值,传递一个空字符串即可。此参数可能会显示在账号选择器中,具体取决于浏览器。
  • pubKeyCredParams:指定 RP(信赖方)支持的公钥算法。我们建议将其设置为 [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]。因为这样设置后,即可支持采用 P-256 和 RSA PKCS#1 的 ECDSA,继而实现全面覆盖。
  • excludeCredentials:已注册的凭据 ID 的列表。通过提供已注册凭据 ID 的列表,防止同一设备重复注册两次。如果提供了 transports 成员,该成员应包含在注册每个凭据期间调用 getTransports() 的结果。
  • authenticatorSelection.authenticatorAttachment:如果此通行密钥创建操作是从密码升级而来(例如在登录后通过促销活动升级),请将此值与 hint: ['client-device'] 一起设置为 "platform""platform" 表示 RP 需要平台身份验证器(嵌入到平台设备的身份验证器),该身份验证器不会提示用户插入 USB 安全密钥等。用户可以使用更简单的方式创建通行密钥。
  • authenticatorSelection.requireResidentKey:将其设置为布尔值 true可检测到的凭据(常驻密钥)会将用户信息存储到通行密钥中,并允许用户在进行身份验证时选择账号。
  • authenticatorSelection.userVerification:用于指示是必须 ("required")、首选 ("preferred") 还是不建议 ("discouraged") 使用设备屏幕锁定功能进行用户验证。默认值为 "preferred",表示身份验证器可以跳过用户验证。请将此属性设置为 "preferred" 或省略该属性。

我们建议在服务器上构建对象,使用 Base64网址 对 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,等待用户通过显示模态对话框进行互动。

Browser Support

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

Source

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

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

使用设备的屏幕锁定功能验证用户身份后,系统会创建通行密钥,并解析 Promise,从而向前端返回 PublicKeyCredential 对象。

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

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

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

  • id:所创建通行密钥的 Base64网址 编码 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(),则发出信号。
  • 在为用户账号创建并注册通行密钥后,向用户发送通知。

资源

后续步骤: 通过表单自动填充功能实现通行密钥登录