概览
下面简要概述了通行密钥注册所涉及的关键步骤:
- 定义用于创建通行密钥的选项。将它们发送给客户端,以便您将它们传递给通行密钥创建调用:Web 上的 WebAuthn API 调用
navigator.credentials.create
和 Android 上的credentialManager.createCredential
。用户确认创建通行密钥后,通行密钥创建调用会得到解决,并返回凭据PublicKeyCredential
。 - 验证凭据并将其存储在服务器上。
以下各部分将深入探讨每个步骤的具体细节。
创建凭据创建选项
您需要在服务器上执行的第一步是创建 PublicKeyCredentialCreationOptions
对象。
为此,请依赖您的 FIDO 服务器端库。它通常会提供一个实用函数,可为您创建这些选项。例如,SimpleWebAuthn 提供 generateRegistrationOptions
。
PublicKeyCredentialCreationOptions
应包含创建通行密钥所需的一切信息:有关用户、RP 的信息,以及有关您要创建的凭据属性的配置。定义完所有这些内容后,根据需要将它们传递给 FIDO 服务器端库中负责创建 PublicKeyCredentialCreationOptions
对象的函数。
PublicKeyCredentialCreationOptions
的某些字段可以是常量。其他变量应在服务器上动态定义:
rpId
:如需在服务器上填充 RP ID,请使用可提供 Web 应用主机名的服务器端函数或变量,例如example.com
。user.name
和user.displayName
:如需填充这些字段,请使用已登录用户的会话信息(如果用户在注册时创建通行密钥,则使用新用户账号信息)。user.name
通常是电子邮件地址,并且对于 RP 是唯一的。user.displayName
是一个简单易记的名称。请注意,并非所有平台都会使用displayName
。user.id
:在创建账号时生成的随机唯一字符串。它应该是永久性的,不同于可能可修改的用户名。用户 ID 用于标识账号,但不得包含任何个人身份信息 (PII)。您的系统中可能已有用户 ID,但如果需要,请专门为通行密钥创建一个用户 ID,以确保其中不包含任何 PII。excludeCredentials
:现有凭据 ID 的列表,用于防止重复使用通行密钥提供程序的通行密钥。如需填充此字段,请在数据库中查找此用户的现有凭据。如需查看详情,请参阅防止在已存在通行密钥时创建新的通行密钥。challenge
:对于凭据注册,除非您使用证明(一种更高级的技术,用于验证通行密钥提供程序的身份及其发出的数据),否则质询并不相关。不过,即使您不使用证明,挑战仍为必填字段。如需了解如何创建安全的身份验证质询,请参阅服务器端通行密钥身份验证。
编码和解码

PublicKeyCredentialCreationOptions
。challenge
、user.id
和 excludeCredentials.credentials
必须在服务器端编码为 base64URL
,以便通过 HTTPS 传送 PublicKeyCredentialCreationOptions
。PublicKeyCredentialCreationOptions
包含属于 ArrayBuffer
的字段,因此不受 JSON.stringify()
支持。这意味着,目前为了通过 HTTPS 传送 PublicKeyCredentialCreationOptions
,必须在服务器上使用 base64URL
手动对某些字段进行编码,然后在客户端上进行解码。
- 在服务器上,编码和解码通常由 FIDO 服务器端库负责处理。
- 在客户端,目前需要手动进行编码和解码。未来,您将可以更轻松地完成此操作:我们将提供一种将 JSON 格式的选项转换为
PublicKeyCredentialCreationOptions
的方法。查看 Chrome 中实现的状态。
示例代码:创建凭据创建选项
在示例中,我们使用的是 SimpleWebAuthn 库。在此处,我们将公钥凭据选项的创建交给其 generateRegistrationOptions
函数。
import { generateRegistrationOptions, verifyRegistrationResponse, generateAuthenticationOptions, verifyAuthenticationResponse } from '@simplewebauthn/server'; import { isoBase64URL } from '@simplewebauthn/server/helpers'; router.post('/registerRequest', csrfCheck, sessionCheck, async (req, res) => { const { user } = res.locals; // Ensure you nest verification function calls in try/catch blocks. // If something fails, throw an error with a descriptive error message. // Return that message with an appropriate error code to the client. try { // `excludeCredentials` prevents users from re-registering existing // credentials for a given passkey provider const excludeCredentials = []; const credentials = Credentials.findByUserId(user.id); if (credentials.length > 0) { for (const cred of credentials) { excludeCredentials.push({ id: isoBase64URL.toBuffer(cred.id), type: 'public-key', transports: cred.transports, }); } } // Generate registration options for WebAuthn create const options = await generateRegistrationOptions({ rpName: process.env.RP_NAME, rpID: process.env.HOSTNAME, userID: user.id, userName: user.username, userDisplayName: user.displayName || '', attestationType: 'none', excludeCredentials, authenticatorSelection: { authenticatorAttachment: 'platform', requireResidentKey: true }, }); // Keep the challenge in the session req.session.challenge = options.challenge; return res.json(options); } catch (e) { console.error(e); return res.status(400).send({ error: e.message }); } });
存储公钥

navigator.credentials.create
返回一个 PublicKeyCredential
对象。当 navigator.credentials.create
在客户端上成功解析时,表示已成功创建通行密钥。返回一个 PublicKeyCredential
对象。
PublicKeyCredential
对象包含一个 AuthenticatorAttestationResponse
对象,该对象表示通行密钥提供方对客户端创建通行密钥的指令做出的响应。它包含有关新凭据的信息,您作为 RP 需要这些信息才能在稍后对用户进行身份验证。如需详细了解 AuthenticatorAttestationResponse
,请参阅附录:AuthenticatorAttestationResponse
。
将 PublicKeyCredential
对象发送到服务器。收到后,请验证该电子邮件地址。
将此验证步骤交给 FIDO 服务器端库。它通常会提供一个用于此目的的实用函数。例如,SimpleWebAuthn 提供 verifyRegistrationResponse
。如需了解幕后发生的情况,请参阅附录:验证注册响应。
验证成功后,将凭据信息存储在数据库中,以便用户日后可以使用与该凭据关联的通行密钥进行身份验证。
使用专用表来存储与通行密钥关联的公钥凭据。一个用户只能有一个密码,但可以有多个通行密钥,例如通过 Apple iCloud 钥匙串同步的通行密钥和通过 Google 密码管理工具同步的通行密钥。
以下是一个可用于存储凭据信息的架构示例:
- 用户表:
user_id
:主要用户 ID。用户的随机唯一永久 ID。将其用作 Users 表的主键。username
. 用户定义的可修改用户名。passkey_user_id
:无个人身份信息的特定于通行密钥的用户 ID,在您的注册选项中以user.id
表示。当用户稍后尝试进行身份验证时,验证器将在其身份验证响应中提供此passkey_user_id
(以userHandle
形式)。我们建议您不要将passkey_user_id
设置为主键。主键往往会成为系统中的事实 PII,因为它们被广泛使用。
- 公钥凭据表:
id
:凭据 ID。将其用作公钥凭据表的主键。public_key
:凭据的公钥。passkey_user_id
:用作外键,以建立与 Users 表的关联。backed_up
:如果通行密钥由通行密钥提供方同步,则表示该通行密钥已备份。如果您想在未来考虑为持有backed_up
通行密钥的用户放弃密码,那么存储备份状态会很有用。您可以通过检查authenticatorData
中的 BE 标志来确认通行密钥是否已备份,也可以使用 FIDO 服务器端库功能(通常可让您轻松访问此信息)来确认。存储备份资格有助于解答潜在的用户咨询。name
:凭据的可选显示名称,用于让用户为凭据指定自定义名称。transports
:一个由传输组成的数组。存储传输对于身份验证用户体验非常有用。当传输可用时,浏览器可以相应地显示与通行密钥提供程序用于与客户端通信的传输相匹配的界面,尤其是在allowCredentials
不为空的重新身份验证使用情形中。
为了提升用户体验,还可以存储其他有用的信息,包括通行密钥提供方、凭据创建时间和上次使用时间等。如需了解详情,请参阅通行密钥界面设计。
示例代码:存储凭据
我们在示例中使用了 SimpleWebAuthn 库。在此处,我们将注册响应验证交给其 verifyRegistrationResponse
函数。
import { isoBase64URL } from '@simplewebauthn/server/helpers'; router.post('/registerResponse', csrfCheck, sessionCheck, async (req, res) => { const expectedChallenge = req.session.challenge; const expectedOrigin = getOrigin(req.get('User-Agent')); const expectedRPID = process.env.HOSTNAME; const response = req.body; // This sample code is for registering a passkey for an existing, // signed-in user // Ensure you nest verification function calls in try/catch blocks. // If something fails, throw an error with a descriptive error message. // Return that message with an appropriate error code to the client. try { // Verify the credential const { verified, registrationInfo } = await verifyRegistrationResponse({ response, expectedChallenge, expectedOrigin, expectedRPID, requireUserVerification: false, }); if (!verified) { throw new Error('Verification failed.'); } const { aaguid, credentialPublicKey, credentialID, credentialBackedUp } = registrationInfo; // Name the credential based on AAGUID const name = aaguid === undefined || aaguid === '000000-0000-0000-0000-00000000' ? req.useragent?.platform : aaguids[aaguid].name; const base64CredentialID = isoBase64URL.fromBuffer(credentialID); const base64PublicKey = isoBase64URL.fromBuffer(credentialPublicKey); // Existing, signed-in user const { user } = res.locals; // Save the credential await Credentials.update({ id: base64CredentialID, passkey_user_id: user.passkey_user_id, publicKey: base64PublicKey, name, aaguid, transports: response.response.transports, backed_up: credentialBackedUp, registered_at: new Date().getTime() }); // Kill the challenge for this session delete req.session.challenge; return res.json(user); } catch (e) { delete req.session.challenge; console.error(e); return res.status(400).send({ error: e.message }); } });
附录:AuthenticatorAttestationResponse
AuthenticatorAttestationResponse
包含两个重要对象:
response.clientDataJSON
是 客户端数据的 JSON 版本,在 Web 上是指浏览器看到的数据。它包含 RP 来源、质询和androidPackageName
(如果客户端是 Android 应用)。作为 RP,读取clientDataJSON
可让您访问浏览器在create
请求时看到的信息。response.attestationObject
包含两项信息:attestationStatement
,除非您使用证明,否则此值无关紧要。authenticatorData
是通行密钥提供方看到的数据。作为 RP,读取authenticatorData
可让您访问密码密钥提供方在create
请求时看到并返回的数据。
authenticatorData
包含与新创建的通行密钥关联的公钥凭据的相关基本信息:
- 公钥凭据本身及其唯一的凭据 ID。
- 与凭据关联的 RP ID。
- 用于描述创建通行密钥时用户状态的标志:用户是否实际在场,以及用户是否已成功通过验证(请参阅 userVerification 深入分析)。
- AAGUID 是通行密钥提供方(例如 Google 密码管理工具)的标识符。您可以根据 AAGUID 识别通行密钥提供方,并在通行密钥管理页面中显示其名称。(请参阅通过 AAGUID 确定通行密钥提供方)
即使 authenticatorData
嵌套在 attestationObject
中,无论您是否使用证明,您的通行密钥实现都需要它所包含的信息。authenticatorData
经过编码,包含以二进制格式编码的字段。您的服务器端库通常会处理解析和解码。如果您未使用服务器端库,请考虑利用 getAuthenticatorData()
客户端来节省一些服务器端解析和解码工作。
附录:注册响应的验证
在底层,验证注册响应包括以下检查:
- 确保 RP ID 与您的网站相符。
- 确保请求的来源是您网站的预期来源(主网站网址、Android 应用)。
- 如果您需要用户验证,请确保用户验证标志
authenticatorData.uv
为true
。 - 用户存在标志
authenticatorData.up
通常应为true
,但如果凭据是有条件创建的,则应为false
。 - 检查客户端是否能够提供您给它的挑战。如果您不使用证明,则此检查并不重要。不过,实现此检查是一项最佳实践:如果您日后决定使用证明,此检查可确保您的代码已做好准备。
- 确保凭据 ID 尚未注册给任何用户。
- 验证通行密钥提供方用于创建凭据的算法是否是您列出的算法(在
publicKeyCredentialCreationOptions.pubKeyCredParams
的每个alg
字段中,该字段通常在服务器端库中定义,您无法看到)。这样可确保用户只能注册您选择允许的算法。
如需了解详情,请查看 SimpleWebAuthn 的 verifyRegistrationResponse
源代码,或深入了解规范中的完整验证列表。