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

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

网站通过使用通行密钥代替密码,可提高用户账号的安全性并简化用户账号的管理和使用,让用户无需输入密码即可登录。借助通行密钥,用户只需使用指纹、人脸或设备 PIN 码即可登录网站或应用。

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

工作原理

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

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

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

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

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

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

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

兼容性

大多数浏览器都支持 WebAuthn,但仍存在一些差距。请参阅 Device Support - passkeys.dev,了解哪些浏览器和操作系统组合支持创建通行密钥。

创建新通行密钥

以下是前端应如何处理创建新通行密钥的请求。

功能检测

在显示“创建新通行密钥”按钮之前,请检查以下情况:

  • 浏览器支持使用 PublicKeyCredential 的 WebAuthn。

浏览器支持

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

来源

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

浏览器支持

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

来源

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

浏览器支持

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

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

调用 WebAuthn API 以创建通行密钥

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

浏览器支持

  • Chrome:60.
  • Edge:18.
  • Firefox:60.
  • Safari: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.com 上或 example.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 对象返回给前端。

承诺可能会因各种原因而被拒绝。您可以通过检查 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
  • 公钥

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

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

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

资源