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

通行密钥使用户账号更加安全、更简单、更易于使用。

使用通行密钥代替密码,网站可以有效提高用户帐号的安全性、更简单、易用且无密码。借助通行密钥,用户只需使用指纹、面孔或设备 PIN 码即可登录网站或应用。

必须先创建通行密钥、将其与用户帐号关联,并将其公钥存储在您的服务器上,然后用户才能使用该通行密钥进行登录。

运作方式

在以下情况下,系统可能会要求用户创建通行密钥:

  • 当用户使用密码登录时。
  • 当用户通过其他设备使用通行密钥登录时(即 authenticatorAttachmentcross-platform)。
  • 通过专门的页面,用户可以在其中管理通行密钥。

如需创建通行密钥,请使用 WebAuthn API

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

  • 后端:此后端服务器保存着帐号数据库,该数据库存储有公钥和与通行密钥有关的其他元数据。
  • 前端:与浏览器通信并向后端发送提取请求的前端。
  • 浏览器:运行 JavaScript 的用户浏览器。
  • 身份验证器:用户的身份验证器,负责创建并存储通行密钥。这可能与浏览器在同一设备上(例如,使用 Windows Hello 时)或其他设备(如手机)。
通行密钥注册示意图

向现有用户账号添加新通行密钥的过程如下:

  1. 用户登录网站。
  2. 用户登录后,他们会请求在前端创建通行密钥,例如按“创建通行密钥”按钮。
  3. 前端会从后端请求信息以创建通行密钥,例如用户信息、质询和要排除的凭据 ID。
  4. 前端调用 navigator.credentials.create() 来创建通行密钥。此调用返回一个 promise。
  5. 在用户使用设备的屏幕锁定功能表示同意后,系统就会创建通行密钥。系统会解析 promise,并将公钥凭据返回给前端。
  6. 前端将公钥凭据发送到后端,并存储凭据 ID 以及与用户帐号关联的公钥以用于将来的身份验证。

兼容性

大多数浏览器都支持 WebAuthn,但也存在一些小问题。如需了解哪些浏览器和操作系统组合支持创建通行密钥,请参阅设备支持 - passkeys.dev

创建新的通行密钥

下方展示了前端应如何在收到创建新通行密钥的请求后进行操作。

功能检测

在显示“创建新的通行密钥”按钮之前,请检查是否满足以下条件:

  • 浏览器支持 WebAuthn。
  • 设备支持平台身份验证器(可以创建通行密钥并使用通行密钥进行身份验证)。
  • 浏览器支持 WebAuthn 条件界面
// 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()

  • challenge:服务器在 ArrayBuffer 中为此项注册生成的质询。这是一项必需的操作,不过在注册期间不会用到,除非执行证明操作 - 这个高级主题在此未涉及。
  • user.id:用户的唯一 ID。该值必须是一个 ArrayBuffer,其中不得包含个人身份信息,例如电子邮件地址或用户名。使用系统为每个帐号生成的 16 字节随机值即可。
  • user.name:此字段应包含用户可以识别的帐号的唯一标识符,例如电子邮件地址或用户名。此 ID 将显示在帐号选择器中。(如果使用用户名,请使用与密码身份验证相同的值。)
  • user.displayName:此字段是必填的,更易用的帐号名称。该名称不必是唯一的,可以是用户选择的名称。如果您的网站没有适合此处的值,传递一个空字符串即可。此参数可能会显示在帐号选择器中,具体取决于浏览器。
  • excludeCredentials:通过提供已注册凭据 ID 的列表来防止注册同一设备。transports 成员(如果提供)应包含每个凭据注册期间调用 getTransports() 的结果。

调用 WebAuthn API 来创建通行密钥

调用 navigator.credentials.create() 以创建新的通行密钥。该 API 会返回一个 promise,等待用户与之互动,并显示模态对话框。

const publicKeyCredentialCreationOptions = {
  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,
  }
};

const credential = await navigator.credentials.create({
  publicKey: publicKeyCredentialCreationOptions
});

// Encode and send the credential to the server for verification.  

上面未说明的参数如下:

  • rp.id:RP ID 是一个网域,网站可以指定其网域或可注册后缀。例如,如果 RP 的来源为 https://login.example.com:1337,则 RP ID 可以是 login.example.comexample.com。如果 RP ID 指定为 example.com,则用户可以在 login.example.comexample.com 上的任何子网域上进行身份验证。

  • rp.name:RP 的名称。

  • pubKeyCredParams:此字段用于指定 RP 支持的公钥算法。我们建议将其设置为 [{alg: -7, type: "public-key"},{alg: -257, type: "public-key"}]。这指定支持采用 P-256 和 RSA PKCS#1 的 ECDSA,并且支持这些实现可以实现全面的覆盖。

  • authenticatorSelection.authenticatorAttachment:如果创建的通行密钥是基于密码的升级(例如在登录后的推广活动中),请将此项设为 "platform""platform" 表示 RP 需要平台身份验证器(嵌入平台设备的身份验证器),该身份验证器不会提示您插入,例如 USB 安全密钥。用户可以采用更简单的方式来创建通行密钥。

  • authenticatorSelection.requireResidentKey:将其设置为布尔值“true”。可检测到的凭据(常驻密钥)会将用户信息存储到通行密钥中,并可让用户在进行身份验证时选择帐号。

  • authenticatorSelection.userVerification:用于指示使用设备屏幕锁定功能进行的用户验证是 "required""preferred" 还是 "discouraged"。默认值为 "preferred",表示身份验证器可以跳过用户验证。请将此属性设置为 "preferred" 或省略该属性。

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

在用户同意使用设备的屏幕锁定功能之后,系统会创建一个通行密钥,并解析 promise,并将一个 PublicKeyCredential 对象返回给前端。

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

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

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

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

如果您使用库来处理后端上的公钥凭据对象,我们建议您在使用 base64url 对对象进行部分编码后将其发送到后端。

保存凭据

在后端收到公钥凭据后,将其传递给 FIDO 库以处理对象。

然后,您可以将从凭据中检索到的信息存储在数据库中,以备将来使用。以下列表包含一些要保存的典型属性:

  • 凭据 ID(主密钥)
  • 用户 ID
  • 公钥

公钥凭据还包含您可能需要保存在数据库中的以下信息:

如需对用户进行身份验证,请阅读通过表单自动填充功能使用通行密钥登录

资源