密钥的用例

本主题介绍通行密钥的一些用例。

用例 1:引导

在 Web 上启动帐户。

1.1:对用户进行身份验证

此部分适用于信赖方(RP)尚不知道谁正在控制客户端设备。 RP(例如本地存储中的 Cookie 或凭据 ID)没有可用的浏览器项目,尽管目前我们假设用户具有具有 RP 的现有帐户。

若要启动帐户,请向用户提供登录页。

首先,请求用户输入其帐户标识符;通常为用户名或电子邮件地址。

登录

若要支持传递密钥的自动填充 UI,请确保:

  1. 将username和webauthn值添加到用户名输入字段上的任何现有自动完成批注。
<div>
  <label for="username">Username:</label>
  <input name="username" id="loginform.username"
         autocomplete="username webauthn">
</div>
  1. 在页面加载时,使用if语句检查自动填充 UI(条件中介)是否可用,然后使用 <navigator.credentials.get()a0/> 进行调用mediation: "conditional"。
  <script>
    (async () => {
      if (
      typeof window.PublicKeyCredential !== 'undefined'
      && typeof window.PublicKeyCredential.isConditionalMediationAvailable === 'function'
      ) {
        const available = await PublicKeyCredential.isConditionalMediationAvailable();

      if (available) {
          try {
            // Retrieve authentication options for `navigator.credentials.get()`
            // from your server.
            const authOptions = await getAuthenticationOptions();
      // This call to `navigator.credentials.get()` is "set and forget."
      // The Promise will resolve only if the user successfully interacts
      // with the browser's autofill UI to select a passkey.
      const webAuthnResponse = await navigator.credentials.get({
          mediation: "conditional",
      publicKey: {
          ...authOptions,
          // See note about userVerification below.
          userVerification: "preferred",
              }
            });
      // Send the response to your server for verification, and
      // authenticate the user if the response is valid.
      await verifyAutoFillResponse(webAuthnResponse);
          } catch (err) {
          console.error('Error with conditional UI:', err);
          }
        }
      }
    })();
  </script>

上述操作将导致以下情况发生:

  • 从服务器检索身份验证选项。 至少返回一个随机 challenge 并 rpId 与此身份验证请求关联。
  • 当用户与 用户名 字段交互时,浏览器和平台将检查密钥是否存在(在平台验证器中),该密钥是否可用于信赖方。
  • 如果是这种情况,则密码密钥将呈现给用户作为选项进行选择(以及其他可自动填充的凭据,例如浏览器密码管理器中存储的用户名)。 浏览器/平台可能会呈现类似于如下所示的 UI。 不过,确切的外观因平台或外形规格而异:

使用通行密钥登录

  • 如果用户选择通行密钥,则平台 UI 将指导用户完成(通常基于生物识别)的用户验证检查。
  • 如果用户成功通过用户验证,则 navigator.credentials.get() 调用成功并返回 WebAuthn 响应。
  • 如果用户选择密钥以外的凭据,则浏览器/平台会选择其他适当的操作(例如自动填充用户名),并且 navigator.credentials.get() 调用无法解析。
  • 如果用户选择“来自另一台设备的 Passkey”选项(确切的文本因平台略有不同),则浏览器/平台将指导用户使用 FIDO2 安全密钥或跨设备身份验证(CDA)流,以使用智能手机或平板电脑的通行密钥向呼叫提供 WebAuthn 响应 navigator.credentials.get() 。
  • 将 WebAuthn 响应发送到服务器以进行验证和其他安全检查。 如果所有检查都成功,请为此用户开始经过身份验证的会话。

这就是为什么这称为 WebAuthn 的条件 UI (或者更常见的 是自动填充 UI)模式(平台验证器 UI,引导用户通过验证或使用其手机)仅在用户在此设备上具有密钥(或选择“其他设备”选项)时才显示。

正如你所看到的,在此模式下, navigator.credentials.get() 调用要么成功,要么不是因为它永远不会解析。 如果成功,调用的结果将同时显示用户 ID 和已签名的 WebAuthn 断言,信赖方(RP)将使用该断言对用户进行身份验证。

如果调用未成功,则应执行 旧 用户身份验证。 你将从第一页获取用户名,然后在后续页面中向用户提供适当的进一步登录质询(例如密码、响应短信质询等)。 例如,如果用户忘记了密码,或者无法传递常规登录质询,这些帐户可能包括 帐户恢复 步骤。 用户通过所有登录质询后,会被视为经过身份验证并登录。

如果用户没有信赖方(RP)的帐户,通常会在登录页上向用户提供创建帐户的选项。 如果用户选择该选项,你将从中收集必要的信息以打开新帐户。 如果他们成功打开新帐户,则它们也会被视为经过身份验证并登录。

用户登录后,可能需要为其设置新的通行密钥。 对于以下任一情况,请执行以下操作:

  • 用户通过传递非密码登录质询(例如使用密码)在设备上启动其帐户。
  • 用户刚刚在信赖方(RP)创建了一个新帐户,因此他们被视为已登录。
  • 用户使用的是通行密钥,但他们使用了与他们当前使用的设备不同的设备(通过选择上面示例中所示的“另一台设备”)。 可以通过检查返回的 PublicKeyCredential 对象中的 authenticatorAttachment 属性来确认这一点。

1.2:跨设备身份验证

如果用户使用另一台设备(例如手机、平板电脑或 FIDO2 安全密钥)的通行密钥,身份验证响应(getAssertion)中的 authenticatorAttachment

在这种情况下,为用户提供在本地设备上创建密钥的选项。 这将导致将来更无缝的用户体验,因为用户不需要使用其他设备。

在此设备上设置密钥!

1.3:有关用户验证的注释

本指南将 userVerification 设置为preferred,这意味着,用户验证将尽可能尝试。

某些设备(如台式计算机和较旧的笔记本电脑)可能没有生物识别传感器。 在这些设备上,如果 userVerification 设置为 required,则可能会要求用户使用 passkey 为每个登录输入其系统登录密码。 这对他们来说可能令人沮丧。

使用时 preferred ,某些平台验证器在设备具有生物识别传感器时始终需要用户验证检查,但可能会跳过设备上没有用户验证的用户验证。

用户验证结果(在验证器数据标志中传达)将反映实际的用户验证结果,并且应始终根据服务器上的要求进行验证。

1.4:选择用户加入 passkey

首先,使用其他登录方法(包括多重身份验证)验证用户是否足够强地进行身份验证。

其次,通过调用确保用户的设备和操作系统(OS)组合支持密钥:

PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()

如果支持通行密钥,则返回 true该密钥。 如果它们不受支持,则它将返回 false,应中止通行密钥注册流。

向用户提供选择加入或“追加销售”模式/间隙或页面以创建密钥:

使用 passkey 更快、更安全的登录!

提示

为了确保用户获得完全知情的同意,请考虑显示(或链接到)较长的说明,说明能够解锁当前设备的所有用户将能够访问信赖方(RP)的帐户。

如果用户同意,请使用以下示例中所示的选项进行调用 navigator.credentials.create() :

navigator.credentials.create({
  publicKey: {
    rp: {
      // User-friendly name of your service.
      name: "Passkeys Developer",
      // Relying party (RP) identifier (hostname/FQDN).
      id: passkeys.contoso"
    },

    user: {
      // Persistent, unique identifier for the user account in your backend.
      id: Uint8Array.from("0525bc79-5a63-4e47-b7d1-597e25f5caba", c => c.charCodeAt(0)),
      // User-friendly identifier often displayed to the user (for example, email address).
      name: "amanda@contoso.com",
      // Human-readable display name, sometimes displayed by the client.
      displayName: "Amanda Brady"
    },
    // The challenge is a buffer of cryptographically random bytes generated on your backend,
    // and should be tightly bound to the current user session.
    challenge: Uint8Array.from("XZJscsUqtBH7ZB90t2g0EbZTZYlbSRK6lq7zlN2lJKuoYMnp7Qo2OLzD7xawL3s", c => c.charCodeAt(0)),
    pubKeyCredParams: [
      // An array of objects describing what public key types are acceptable to a server.
      {
        "type": "public-key",
        "alg": -7 // EC P256
      },
      {
        "type": "public-key",
        "alg": -257 // RSA
      }
    ],
    excludeCredentials: [
      // Array of credential IDs for existing passkeys tied to the user account.
      // This avoids creating a new passkey in an authenticator that already has 
      // a passkey tied to the user account.
      {
        // Example only.
        type: "public-key",
        id: new Uint8Array([21, 31, 56, ...]).buffer
      },
      {
        // Example only.
        type: "public-key",
        id: new Uint8Array([21, 31, 56, ...]).buffer
      }
    ],
    authenticatorSelection: {
      // Tells the authenticator to create a passkey.
      residentKey: "required",
      // Tells the client/authenticator to request user verification where possible;
      // for example, a biometric or a device PIN.
      userVerification: "preferred"
    },
    "extensions": {
      // Returns details about the passkey.
      "credProps": true
    }
  }
})

注意

建议大多数信赖方(RP)不指定证明传递参数 attestation (因此默认为无),或改为显式使用值 indirect。 这可以保证最简化的用户体验(由于用户取消创建,平台可能会获得用户对其他类型的证明传递的同意,这可能导致凭据创建失败的较大部分)。

当 WebAuthn 调用解析时,将响应发送到服务器,并将返回的公钥和凭据 ID 与以前经过身份验证的用户帐户相关联。

用例 2:重新身份验证

出于以下任何原因,可能需要使用密钥进行重新身份验证:

  • 用户已注销,现在想要再次登录。
  • 由于处于非活动状态,用户会话已过期,用户希望再次登录。
  • 用户即将执行敏感操作,需要重新确认对用户会话的控制。

若要在上述每种情况下重新对用户进行身份验证,请使用在前面的用例中设置的密钥。 在所有三种情况下,WebAuthn API 调用都是相同的,但你提供的 UI 处理略有不同。 由于特定帐户由你指定,因此平台不会提示用户在你的服务中选择其他帐户。

2.1:敏感操作

让我们首先查看 UI,原因为第三个—当重新对敏感操作进行身份验证时,请检查用户是否具有至少一个密钥的凭据 ID。

如果没有此类凭据 ID 可用,则提供适合重新身份验证的传统登录质询,例如:

让我们确保它是你 1

提示

建议在此登录质询页上,用户无法更改其帐户标识符。 此外,登录质询应该是设备未经授权的用户无法传递的内容。

另一方面,如果为用户找到至少一个密钥凭据 ID,则可以使用密码密钥进行重新身份验证:

让我们确保它是你 2

当用户准备就绪(在上面的示例中,单击“转到”按钮时),调用 navigator.credentials.get(),传入所有用户的通行密钥凭据 ID:

navigator.credentials.get({
  publicKey: {
    challenge: ...,
    rpId: ...,
     allowCredentials: [{
      type: "public-key",      
      id: new UInt8Array([21, 31, 56, ...]).buffer,
    }, {
      type: "public-key",
      id: new UInt8Array([21, 31, 56, ...]).buffer,
    }, {
      ...
    }],
    // see note below
    userVerification: "preferred", 
  }
});

注意

请务必阅读之前用例中有关 userVerification 的指南。

如果用户改为单击“尝试其他方式”,则应向他们提供其他登录方法(密码等)以重新进行身份验证(假设用户具有其他登录方法可供他们使用)。

2.2:会话和注销已过期

现在,我们将检查触发重新身份验证的情况,因为用户已自行注销,或者信赖方 (RP) 已过期用户会话。 为此,RP 必须保留某种形式的用户会话状态,提醒他们以前登录的帐户,即使他们认为用户已注销(可以使用浏览器项目(如 Cookie 或本地存储)来实现)。

注意

信赖方(RP)可能会选择将注销视为一项全面的操作,从而删除对用户标识的所有引用。 此类 RP 应像启动帐户一样处理后续登录,并重复前面介绍的步骤。

然后,作为 RP,可能会提供如下所示的登录页:

欢迎回来!1

如果用户单击“使用其他帐户”,则应输入帐户启动流(如前面的用例所述),重复其中的步骤,平台将允许用户选择要使用的帐户。

注意

在这种情况下,还应让用户能够完全删除建议的帐户,将其列在登录页上。

但是,如果用户单击“登录身份”按钮,请检查你是否至少有一个与用户关联的通行密钥凭据 ID。 如果没有可用的凭据 ID,则提供适用于重新身份验证的传统登录质询,例如:

欢迎回来!2

另一方面,如果为用户找到至少一个密钥凭据 ID,则可以使用密码密钥进行重新身份验证:

欢迎回来!3

用户准备就绪(在上面的示例中,单击“转到”按钮时),调用 navigator.credentials.get(),完全如已显示(即,通过传入所有用户的通行密钥凭据 ID)。

如果用户改为单击“尝试其他方式”,则应向他们提供其他登录方法(密码等)以重新进行身份验证(假设用户具有其他登录方法可供他们使用)。

后续步骤

接下来,请参阅 用于传递密钥的工具和库。

更多信息