О практической работе
1. 准备工作
网站通过使用通行密钥代替密码,可提高用户帐号的安全性并简化用户帐号的管理和使用。借助通行密钥,用户可以使用设备的屏幕锁定功能(例如指纹锁、人脸识别锁或设备 PIN 码)来登录网站或应用。必须先创建通行密钥、将其与用户帐号关联,并将其公钥存储在服务器上,之后用户才能使用该通行密钥进行登录。
在此 Codelab 中,您需要将一个基于表单的基本用户名和密码登录方式转换为支持通行密钥登录,具体而言,您需要实现以下组件:
- 有一个按钮,以便在用户登录后创建通行密钥。
- 有一个界面,用来显示已注册通行密钥的列表。
- 有一个现有登录表单,让用户能够通过表单自动填充功能,使用已注册的通行密钥进行登录。
前提条件
- 对 JavaScript 有基本的了解
- 对通行密钥有基本的了解
- 对 Web Authentication API (WebAuthn) 有基本的了解
学习内容
- 如何创建通行密钥。
- 如何使用通行密钥对用户进行身份验证。
- 如何在表单中显示通行密钥作为一个登录选项。
所需条件
以下任一设备组合:
- Google Chrome 及搭载 Android 9 或更高版本的 Android 设备,最好带有生物识别传感器。
- Chrome 及搭载 Windows 10 或更高版本的 Windows 设备。
- Safari 16 或更高版本,及搭载 iOS 16 或更高版本的 iPhone 或搭载 iPadOS 16 或更高版本的 iPad。
- Safari 16 或更高版本或 Chrome,及搭载 macOS Ventura 或更高版本的 Apple 桌面设备。
2. 进行设置
在此 Codelab 中,您将使用一项名为 Glitch 的服务。借助该服务,您可以使用 JavaScript 修改客户端和服务器端代码,并且通过浏览器即可部署这些代码。
打开项目
- 在 Glitch 中打开项目。
- 点击 Remix 复刻 Glitch 项目。
- 在 Glitch 底部的导航菜单中,依次点击 Preview(预览)> Preview in a new window(在新窗口中预览)。您的浏览器会打开另一个标签页。
检查网站的初始状态
- 在“预览”标签页中,输入一个随机用户名,然后点击 Next(下一步)。
- 输入一个随机密码,然后点击 Sign-in(登录)。虽然该密码会被忽略,但您仍然会通过身份验证并登录首页。
- 您可以在此修改显示名称;这也是您在该初始状态下唯一可以执行的操作。
- 点击 Sign out(退出)。
在此状态下,用户每次登录时都必须输入密码。您可以在此表单中添加通行密钥支持,以便用户可以使用设备的屏幕锁定功能进行登录。您可以访问 https://passkeys-codelab.glitch.me/,尝试一下结束状态的效果。
如需详细了解通行密钥的工作原理,请参阅通行密钥的工作原理。
3. 添加用以创建通行密钥的功能
若要让用户能够使用通行密钥进行身份验证,您需要先提供一项功能,让用户能够创建和注册通行密钥,并将其公钥存储在服务器上。
您需要允许用户在使用密码登录后创建通行密钥,并添加一个界面,让用户能够在该界面上创建通行密钥并通过 /home
页面查看所有已注册的通行密钥。在下一部分中,您需要创建一个用于创建和注册通行密钥的函数。
创建 registerCredential()
函数
- 在 Glitch 中,打开
public/client.js
文件,然后滚动到文件末尾。 - 在相关注释后面,添加下面的
registerCredential()
函数:
public/client. js
// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
// TODO: Add an ability to create a passkey: Create a credential.
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
};
此函数将在服务器上创建并注册通行密钥。
从服务器端点获取质询和其他选项
创建通行密钥之前,您需要从服务器获取要传入 WebAuthn 的参数(包括质询)。WebAuthn 是一款浏览器 API,可让用户创建通行密钥并使用该通行密钥对用户进行身份验证。现在,我们已在此 Codelab 中为您提供了可对此类参数做出响应的服务器端点。
- 如需从服务器端点获取质询和其他选项,请将下面的代码添加到
registerCredential()
函数正文的相关注释后面:
public/client.js
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/registerRequest');
以下代码段包含您从服务器收到的选项示例:
{
challenge: *****,
rp: {
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', 'hybrid'],
}],
authenticatorSelection: {
authenticatorAttachment: "platform",
requireResidentKey: true,
}
}
服务器与客户端之间的协议不在 WebAuthn 规范范畴内。不过,此 Codelab 的服务器会返回一个 JSON,该 JSON 与传递给 WebAuthn navigator.credentials.create()
API 的 PublicKeyCredentialCreationOptions
字典极其相似。
下表列出了 PublicKeyCredentialCreationOptions
字典中的一些重要参数,但并非详尽无遗:
参数 | 说明 |
服务器在 | |
用户的唯一 ID。该值必须是一个 | |
此字段应包含用户可以识别的帐号唯一标识符,例如电子邮件地址或用户名。它会显示在帐号选择器中。(如果要使用用户名,请使用您在密码身份验证登录方法中使用的用户名。) | |
此字段是帐号的易记名称,这是一个可选参数。该名称不必是唯一的,可以是用户选择的名称。如果您的网站没有适合此参数的值,传递一个空字符串即可。此参数可能会显示在帐号选择器中,具体取决于浏览器。 | |
依赖方 (RP) ID 是一个网域。网站可以指定自己的网域,也可以指定一个可注册后缀。例如,如果 RP 的来源为 https://login.example.com:1337,则 RP ID 可以是 | |
此字段用于指定 RP 支持的公钥算法。我们建议将其设置为 | |
提供已注册凭据 ID 的列表,以防止同一设备重复注册两次。如果该参数包含 | |
设为 | |
设为布尔值 | |
设为 |
创建凭据
- 在
registerCredential()
函数正文相关注释后面,将一些使用 Base64URL 编码的参数转换回二进制数据,具体而言就是user.id
和challenge
字符串以及excludeCredentials
数组中包含的id
字符串实例:
public/client.js
// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);
if (options.excludeCredentials) {
for (let cred of options.excludeCredentials) {
cred.id = base64url.decode(cred.id);
}
}
- 在下一行代码中,将
authenticatorSelection.authenticatorAttachment
设为"platform"
,将authenticatorSelection.requireResidentKey
设为true
。这样系统将仅允许使用平台身份验证器(设备本身),并允许使用可检测到的凭据。
public/client.js
// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
authenticatorAttachment: 'platform',
requireResidentKey: true
}
- 在下一行代码中,调用
navigator.credentials.create()
方法来创建凭据。
public/client.js
// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
publicKey: options,
});
调用此方法后,浏览器会尝试通过设备的屏幕锁定功能来验证用户的身份。
向服务器端点注册凭据
用户验证身份后,系统会创建并存储通行密钥。网站会接收一个包含公钥的凭据对象,您可以将该公钥发送给服务器以注册通行密钥。
以下代码段包含一个示例凭据对象:
{
"id": *****,
"rawId": *****,
"type": "public-key",
"response": {
"clientDataJSON": *****,
"attestationObject": *****,
"transports": ["internal", "hybrid"]
},
"authenticatorAttachment": "platform"
}
下表列出了 PublicKeyCredential
对象中的一些重要参数,但并非详尽无遗:
参数 | 说明 |
所创建通行密钥的 Base64URL 编码 ID。此 ID 有助于浏览器在进行身份验证时确定设备中是否存在匹配的通行密钥。此值必须存储在后端的数据库中。 | |
| |
| |
| |
设备支持的传输列表: | |
如果是在支持通行密钥的设备上创建此凭据,此参数的值会是 |
如要将凭据对象发送给服务器,请按以下步骤操作:
- 将凭据的二进制参数编码为 Base64URL 格式,以便以字符串的形式将其传递给服务器:
public/client.js
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
credential.authenticatorAttachment = cred.authenticatorAttachment;
}
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject = base64url.encode(cred.response.attestationObject);
// Obtain transports.
const transports = cred.response.getTransports ? cred.response.getTransports() : [];
credential.response = {
clientDataJSON,
attestationObject,
transports
};
- 在下一行代码中,将对象发送给服务器:
public/client.js
return await _fetch('/auth/registerResponse', credential);
当您运行该程序时,服务器会返回 HTTP code 200
,这表示该凭据已注册完毕。
现在,您已拥有完整的 registerCredential()
函数!
查看本部分的解决方案代码
public/client.js
// TODO: Add an ability to create a passkey: Create the registerCredential() function.
export async function registerCredential() {
// TODO: Add an ability to create a passkey: Obtain the challenge and other options from server endpoint.
const options = await _fetch('/auth/registerRequest');
// TODO: Add an ability to create a passkey: Create a credential.
// Base64URL decode some values.
options.user.id = base64url.decode(options.user.id);
options.challenge = base64url.decode(options.challenge);
if (options.excludeCredentials) {
for (let cred of options.excludeCredentials) {
cred.id = base64url.decode(cred.id);
}
}
// Use platform authenticator and discoverable credential.
options.authenticatorSelection = {
authenticatorAttachment: 'platform',
requireResidentKey: true
}
// Invoke the WebAuthn create() method.
const cred = await navigator.credentials.create({
publicKey: options,
});
// TODO: Add an ability to create a passkey: Register the credential to the server endpoint.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// The authenticatorAttachment string in the PublicKeyCredential object is a new addition in WebAuthn L3.
if (cred.authenticatorAttachment) {
credential.authenticatorAttachment = cred.authenticatorAttachment;
}
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const attestationObject =
base64url.encode(cred.response.attestationObject);
// Obtain transports.
const transports = cred.response.getTransports ?
cred.response.getTransports() : [];
credential.response = {
clientDataJSON,
attestationObject,
transports
};
return await _fetch('/auth/registerResponse', credential);
};
4. 构建一个界面来注册和管理通行密钥凭据
现在,既然 registerCredential()
函数已可供使用,接下来您需要一个按钮来调用它。此外,您还需要显示已注册通行密钥的列表。
添加占位符 HTML
- 在 Glitch 中,打开
views/home.html
文件。 - 在相关注释后面,添加一个界面占位符,包括一个用于注册通行密钥的按钮和一个通行密钥列表:
views/home.html
<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
<h3 class="mdc-typography mdc-typography--headline6"> Your registered
passkeys:</h3>
<div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>
div#list
元素是列表的占位符。
检查通行密钥是否受支持
若要仅向其设备支持通行密钥的用户显示通行密钥创建选项,您首先需要检查 WebAuthn 是否可用。若可用,您需要移除 hidden
类以显示创建通行密钥按钮。
若要检查环境是否支持通行密钥,请按以下步骤操作:
- 在
views/home.html
文件末尾相关注释后面,编写一个条件,在window.PublicKeyCredential
、PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable
和PublicKeyCredential.isConditionalMediationAvailable
都为true
时执行。
views/home.html
// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
PublicKeyCredential.isConditionalMediationAvailable) {
- 在条件正文中,检查设备是否可以创建通行密钥,然后检查是否可以在表单中自动填充通行密钥。
views/home.html
try {
const results = await Promise.all([
// Is platform authenticator available in this browser?
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
// Is conditional UI available in this browser?
PublicKeyCredential.isConditionalMediationAvailable()
]);
- 如果满足所有条件,则显示用以创建通行密钥的按钮。否则,显示一则警告消息。
views/home.html
if (results.every(r => r === true)) {
// If conditional UI is available, reveal the Create a passkey button.
createPasskey.classList.remove('hidden');
} else {
// If conditional UI isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
} catch (e) {
console.error(e);
}
} else {
// If WebAuthn isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
以列表形式呈现已注册的通行密钥
- 定义一个
renderCredentials()
函数,用以从服务器提取已注册的通行密钥并将其以列表形式呈现。现在,我们已为您提供了/auth/getKeys
服务器端点来提取登录用户的已注册通行密钥。
views/home.html
// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
const res = await _fetch('/auth/getKeys');
const list = $('#list');
const creds = html`${res.length > 0 ? html`
<mwc-list>
${res.map(cred => html`
<mwc-list-item>
<div class="list-item">
<div class="entity-name">
<span>${cred.name || 'Unnamed' }</span>
</div>
<div class="buttons">
<mwc-icon-button data-cred-id="${cred.id}"
data-name="${cred.name || 'Unnamed' }" @click="${rename}"
icon="edit"></mwc-icon-button>
<mwc-icon-button data-cred-id="${cred.id}" @click="${remove}"
icon="delete"></mwc-icon-button>
</div>
</div>
</mwc-list-item>`)}
</mwc-list>` : html`
<mwc-list>
<mwc-list-item>No credentials found.</mwc-list-item>
</mwc-list>`}`;
render(creds, list);
};
- 在下一行代码中,调用
renderCredentials()
函数,以便在用户进入/home
页面时立即显示已注册的通行密钥(即作为初始化流程显示)。
views/home.html
renderCredentials();
创建并注册通行密钥
若要创建并注册通行密钥,您需要调用在前面实现的 registerCredential()
函数。
若要在点击创建通行密钥按钮时触发 registerCredential()
函数,请按以下步骤操作:
- 在文件中占位符 HTML 后面,找到下面的
import
语句:
views/home.html
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
} from '/client.js';
- 在
import
语句正文的末尾,添加registerCredential()
函数。
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
registerCredential
} from '/client.js';
- 在文件末尾相关注释后面,定义一个
register()
函数来调用registerCredential()
函数和加载界面,并在注册完成后调用renderCredentials()
。这表明浏览器会创建一个通行密钥,并会在出现问题时显示错误消息。
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
try {
// Start the loading UI.
loading.start();
// Start creating a passkey.
await registerCredential();
// Stop the loading UI.
loading.stop();
// Render the updated passkey list.
renderCredentials();
- 在
register()
函数的正文中,捕获异常。如果设备上已存在通行密钥,navigator.credentials.create()
方法会抛出一个InvalidStateError
错误。可通过excludeCredentials
数组对此进行检查。在此示例中,您还会向用户显示一条相关消息。此外,如果用户取消身份验证对话框,则会抛出NotAllowedError
错误。在此示例中,您将以静默方式忽略该错误。
views/home.html
} catch (e) {
// Stop the loading UI.
loading.stop();
// An InvalidStateError indicates that a passkey already exists on the device.
if (e.name === 'InvalidStateError') {
alert('A passkey already exists for this device.');
// A NotAllowedError indicates that the user canceled the operation.
} else if (e.name === 'NotAllowedError') {
Return;
// Show other errors in an alert.
} else {
alert(e.message);
console.error(e);
}
}
};
- 在
register()
函数后面的一行代码中,将register()
函数关联到创建通行密钥按钮的click
事件。
views/home.html
createPasskey.addEventListener('click', register);
查看本部分的解决方案代码
views/home.html
<!-- TODO: Add an ability to create a passkey: Add placeholder HTML. -->
<section>
<h3 class="mdc-typography mdc-typography--headline6"> Your registered
passkeys:</h3>
<div id="list"></div>
</section>
<p id="message" class="instructions"></p>
<mwc-button id="create-passkey" class="hidden" icon="fingerprint" raised>Create a passkey</mwc-button>
views/home.html
// TODO: Add an ability to create a passkey: Create and register a passkey.
import {
$,
_fetch,
loading,
updateCredential,
unregisterCredential,
registerCredential
} from '/client.js';
views/home.html
// TODO: Add an ability to create a passkey: Check for passkey support.
const createPasskey = $('#create-passkey');
// Feature detections
if (window.PublicKeyCredential &&
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable &&
PublicKeyCredential.isConditionalMediationAvailable) {
try {
const results = await Promise.all([
// Is platform authenticator available in this browser?
PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(),
// Is conditional UI available in this browser?
PublicKeyCredential.isConditionalMediationAvailable()
]);
if (results.every(r => r === true)) {
// If conditional UI is available, reveal the Create a passkey button.
createPasskey.classList.remove('hidden');
} else {
// If conditional UI isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
} catch (e) {
console.error(e);
}
} else {
// If WebAuthn isn't available, show a message.
$('#message').innerText = 'This device does not support passkeys.';
}
// TODO: Add an ability to create a passkey: Render registered passkeys in a list.
async function renderCredentials() {
const res = await _fetch('/auth/getKeys');
const list = $('#list');
const creds = html`${res.length > 0 ? html`
<mwc-list>
${res.map(cred => html`
<mwc-list-item>
<div class="list-item">
<div class="entity-name">
<span>${cred.name || 'Unnamed' }</span>
</div>
<div class="buttons">
<mwc-icon-button data-cred-id="${cred.id}" data-name="${cred.name || 'Unnamed' }" @click="${rename}" icon="edit"></mwc-icon-button>
<mwc-icon-button data-cred-id="${cred.id}" @click="${remove}" icon="delete"></mwc-icon-button>
</div>
</div>
</mwc-list-item>`)}
</mwc-list>` : html`
<mwc-list>
<mwc-list-item>No credentials found.</mwc-list-item>
</mwc-list>`}`;
render(creds, list);
};
renderCredentials();
// TODO: Add an ability to create a passkey: Create and register a passkey.
async function register() {
try {
// Start the loading UI.
loading.start();
// Start creating a passkey.
await registerCredential();
// Stop the loading UI.
loading.stop();
// Render the updated passkey list.
renderCredentials();
} catch (e) {
// Stop the loading UI.
loading.stop();
// An InvalidStateError indicates that a passkey already exists on the device.
if (e.name === 'InvalidStateError') {
alert('A passkey already exists for this device.');
// A NotAllowedError indicates that the user canceled the operation.
} else if (e.name === 'NotAllowedError') {
Return;
// Show other errors in an alert.
} else {
alert(e.message);
console.error(e);
}
}
};
createPasskey.addEventListener('click', register);
试用一下
如果您完成了上述所有步骤,即表示您已实现了在网站上创建、注册和显示通行密钥的功能!
如要试用其效果,请按以下步骤操作:
- 在“预览”标签页中,使用随机用户名和密码登录。
- 点击创建通行密钥。
- 使用设备的屏幕锁定功能来验证您的身份。
- 确认您的通行密钥已注册并显示在网页的已注册的通行密钥部分下。
重命名和移除已注册的通行密钥
您应该可以重命名或删除列表中的已注册通行密钥。您可在 Codelab 附带的代码中查看其运作原理。
对于 Chrome,您可在桌面版的 chrome://settings/passkeys 页面或在 Android 设备设置中的密码管理工具页面移除已注册的通行密钥。
如需了解如何重命名和移除在其他平台上注册的通行密钥,请参阅相应平台的支持页面。
5. 添加使用通行密钥进行身份验证的功能
用户现在已能够创建并注册通行密钥,下一步便是用该通行密钥来安全地向您的网站进行身份验证。现在,您需要为自己的网站添加通行密钥身份验证功能。
创建 authenticate()
函数
- 在
public/client.js
文件相关注释后面,创建一个名为authenticate()
的函数,用于在本地验证用户身份,然后向服务器完成验证:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {
// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
};
从服务器端点获取质询和其他选项
在要求用户进行身份验证之前,您需要从服务器获取要传入 WebAuthn 的参数(包括质询)。
- 在
authenticate()
函数正文相关注释后面,调用_fetch()
函数以向服务器发送一个POST
请求:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Obtain the challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');
此 Codelab 的服务器会返回一个 JSON,该 JSON 与传递给 WebAuthn navigator.credentials.get()
API 的 PublicKeyCredentialRequestOptions
字典极其相似。以下代码段包含您应会收到的选项示例:
{
"challenge": *****,
"rpId": "passkeys-codelab.glitch.me",
"allowCredentials": []
}
下表列出了 PublicKeyCredentialRequestOptions
字典中的一些重要参数,但并非详尽无遗:
参数 | 说明 |
服务器在 | |
RP ID 是一个网域,网站可以指定自己的网域,也可以指定一个可注册后缀。此值必须与创建通行密钥时使用的 | |
此属性用于查找符合此身份验证条件的身份验证器。传递空数组或不指定数组,即可让浏览器显示帐号选择器。 | |
设为 |
在本地验证用户身份并获取凭据
- 在
authenticate()
函数正文相关注释后面,将challenge
参数转换回二进制数据:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Locally verify the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
- 将一个空数组传递给
allowCredentials
参数,以便在用户进行身份验证时打开帐号选择器:
public/client.js
// An empty allowCredentials array invokes an account selector by discoverable credentials.
options.allowCredentials = [];
帐号选择器会使用随通行密钥一起存储的用户信息。
- 调用
navigator.credentials.get()
方法并提供mediation: 'conditional'
选项:
public/client.js
// Invoke the WebAuthn get() method.
const cred = await navigator.credentials.get({
publicKey: options,
// Request a conditional UI.
mediation: 'conditional'
});
此选项指示浏览器在表单自动填充项中有条件地建议通行密钥。
验证凭据
用户在本地验证自己的身份后,您应该会收到一个包含签名的凭据对象,您可以在服务器上验证该签名。
以下代码段包含一个示例 PublicKeyCredential
对象:
{
"id": *****,
"rawId": *****,
"type": "public-key",
"response": {
"clientDataJSON": *****,
"authenticatorData": *****,
"signature": *****,
"userHandle": *****
},
authenticatorAttachment: "platform"
}
下表列出了 PublicKeyCredential
对象中的一些重要参数,但并非详尽无遗:
参数 | 说明 |
经过身份验证的通行密钥凭据的 Base64URL 编码 ID。 | |
| |
客户端数据的 | |
身份验证器数据的 | |
签名的 | |
| |
当此凭据来自本地设备时,则返回一个 |
如要将凭据对象发送给服务器,请按以下步骤操作:
- 在
authenticate()
函数正文相关注释后面,对凭据的二进制参数进行编码,以便以字符串的形式将其传递给服务器:
public/client.js
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData = base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);
credential.response = {
clientDataJSON,
authenticatorData,
signature,
userHandle,
};
- 将对象发送给服务器:
public/client.js
return await _fetch(`/auth/signinResponse`, credential);
当您运行该程序时,服务器会返回 HTTP code 200
,这表示该凭据已验证完毕。
现在,您已拥有完整的 authentication()
函数!
查看本部分的解决方案代码
public/client.js
// TODO: Add an ability to authenticate with a passkey: Create the authenticate() function.
export async function authenticate() {
// TODO: Add an ability to authenticate with a passkey: Obtain the
challenge and other options from the server endpoint.
const options = await _fetch('/auth/signinRequest');
// TODO: Add an ability to authenticate with a passkey: Locally verify
the user and get a credential.
// Base64URL decode the challenge.
options.challenge = base64url.decode(options.challenge);
// The empty allowCredentials array invokes an account selector
by discoverable credentials.
options.allowCredentials = [];
// Invoke the WebAuthn get() function.
const cred = await navigator.credentials.get({
publicKey: options,
// Request a conditional UI.
mediation: 'conditional'
});
// TODO: Add an ability to authenticate with a passkey: Verify the credential.
const credential = {};
credential.id = cred.id;
credential.rawId = cred.id; // Pass a Base64URL encoded ID string.
credential.type = cred.type;
// Base64URL encode some values.
const clientDataJSON = base64url.encode(cred.response.clientDataJSON);
const authenticatorData =
base64url.encode(cred.response.authenticatorData);
const signature = base64url.encode(cred.response.signature);
const userHandle = base64url.encode(cred.response.userHandle);
credential.response = {
clientDataJSON,
authenticatorData,
signature,
userHandle,
};
return await _fetch(`/auth/signinResponse`, credential);
};
6. 向浏览器自动填充项添加通行密钥
若用户在初始登录之后再次返回您的网站,您肯定希望让用户尽可能轻松、安全地登录。如果您向登录页面添加了使用通行密钥登录按钮,用户可以点击此按钮,在浏览器的帐号选择器中选择一个通行密钥,然后使用屏幕锁定功能验证自己的身份。
不过,并非所有用户都能够立即从密码改换为通行密钥。这意味着,您暂时还不能移除密码登录选项,直到所有用户都改用通行密钥之后,您才能移除基于密码的登录表单。不过,如果您同时呈现密码表单和通行密钥按钮,则用户将必须费心选择到底要使用哪一种登录方式,这是没有必要的。理想情况下,您应该提供一个简单直接的登录流程。
这正是“条件界面”的用武之地。条件界面是 WebAuthn 的一项功能,借助该功能,您可以使表单输入字段自动填充密码,也可以自动填充通行密钥。如果用户在自动填充建议中点按通行密钥,系统就会要求用户使用设备的屏幕锁定功能在本地验证自己的身份。这是一种无缝衔接的用户体验,因为用户操作与基于密码的登录几乎完全相同。
启用条件界面
如要启用条件界面,您只需在输入字段的 autocomplete
属性中添加一个 webauthn
令牌即可。设置该令牌后,您可以调用 navigator.credentials.get()
方法并使用 mediation: 'conditional'
字符串,以便有条件地触发屏幕锁定功能界面。
- 如要启用条件界面,请在
view/index.html
文件相关注释后面,将现有的用户名输入字段替换为以下 HTML 内容:
view/index.html
<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
type="text"
id="username"
class="mdc-text-field__input"
aria-labelledby="username-label"
name="username"
autocomplete="username webauthn"
autofocus />
检测功能、调用 WebAuthn 并启用条件界面
- 在
view/index.html
文件相关注释后面,将现有import
语句替换为以下代码:
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
$,
_fetch,
loading,
authenticate
} from "/client.js";
此代码会导入您在前面实现的 authenticate()
函数。
- 确认
window.PulicKeyCredential
对象可用,并且PublicKeyCredential.isConditionalMediationAvailable()
方法会返回true
值,然后调用authenticate()
函数:
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
if (
window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable
) {
try {
// Is conditional UI available in this browser?
const cma =
await PublicKeyCredential.isConditionalMediationAvailable();
if (cma) {
// If conditional UI is available, invoke the authenticate() function.
const user = await authenticate();
if (user) {
// Proceed only when authentication succeeds.
$("#username").value = user.username;
loading.start();
location.href = "/home";
} else {
throw new Error("User not found.");
}
}
} catch (e) {
loading.stop();
// A NotAllowedError indicates that the user canceled the operation.
if (e.name !== "NotAllowedError") {
console.error(e);
alert(e.message);
}
}
}
查看本部分的解决方案代码
view/index.html
<!-- TODO: Add passkeys to the browser autofill: Enable conditional UI. -->
<input
type="text"
id="username"
class="mdc-text-field__input"
aria-labelledby="username-label"
name="username"
autocomplete="username webauthn"
autofocus
/>
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
import {
$,
_fetch,
loading,
authenticate
} from '/client.js';
view/index.html
// TODO: Add passkeys to the browser autofill: Detect features, invoke WebAuthn, and enable a conditional UI.
// Is WebAuthn avaiable in this browser?
if (window.PublicKeyCredential &&
PublicKeyCredential.isConditionalMediationAvailable) {
try {
// Is a conditional UI available in this browser?
const cma= await PublicKeyCredential.isConditionalMediationAvailable();
if (cma) {
// If a conditional UI is available, invoke the authenticate() function.
const user = await authenticate();
if (user) {
// Proceed only when authentication succeeds.
$('#username').value = user.username;
loading.start();
location.href = '/home';
} else {
throw new Error('User not found.');
}
}
} catch (e) {
loading.stop();
// A NotAllowedError indicates that the user canceled the operation.
if (e.name !== 'NotAllowedError') {
console.error(e);
alert(e.message);
}
}
}
试用一下
您现已在网站上实现了通行密钥的创建、注册、显示和身份验证。
如要试用其效果,请按以下步骤操作:
- 前往“预览”标签页。
- 如有必要,请退出帐号。
- 点击用户名文本框。随即会出现一个对话框。
- 选择您要登录的帐号。
- 使用设备的屏幕锁定功能来验证您的身份。系统会将您重定向至
/home
页面并登录。