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

通行密钥可让用户账号更安全、更简单、更易用。

网站可以通过使用通行密钥代替密码,让用户帐号更加安全、更简单、易用且无密码。借助通行密钥,用户只需使用其指纹、人脸或设备 PIN 码即可登录网站或应用。

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

运作方式

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

  • 当用户使用密码登录时。
  • 当用户使用另一部设备(即 authenticatorAttachmentcross-platform)的通行密钥登录时。
  • 在用户可以管理其通行密钥的专用页面上。

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

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

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

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

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

兼容性

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

创建新通行密钥

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

功能检测

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

  • 浏览器支持通过 PublicKeyCredential 使用 WebAuthn。

浏览器支持

  • 67
  • 18
  • 60
  • 13

来源

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

浏览器支持

  • 67
  • 18
  • 60
  • 13

来源

  • 浏览器支持搭配 PublicKeyCredenital.isConditionalMediationAvailable() 使用 WebAuthn 条件界面

浏览器支持

  • 108
  • 108
  • 119
  • 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()

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

调用 WebAuthn API 以创建通行密钥

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

浏览器支持

  • 60
  • 18
  • 60
  • 13

来源

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
  • 公钥

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

如需查看更详细的说明,请参阅服务器端通行密钥注册

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

资源