Hướng dẫn này trình bày chi tiết cách cho phép người dùng xem, đổi tên và xoá khoá truy cập đã đăng ký, đồng thời đồng bộ hoá thông tin đăng nhập đã lưu và thông tin hồ sơ người dùng giữa máy chủ và trình quản lý mật khẩu của người dùng bằng Signal API.
Thao tác phía máy chủ
Lớp cơ sở dữ liệu phụ trợ và điểm cuối của bạn PHẢI hỗ trợ các thao tác CRUD phổ biến cho thông tin đăng nhập đã đăng ký cũng như cập nhật thông tin hồ sơ người dùng. Được tách riêng khỏi các thư viện dành riêng cho khung, máy chủ sẽ hiển thị các điểm cuối để:
- Liệt kê tất cả thông tin đăng nhập của người dùng: Tìm nạp tất cả bản ghi
StoredPasskeyCredentialkhớp với mã nhận dạng của người dùng đã đăng nhập. - Cập nhật tên thông tin xác thực: Chấp nhận biệt hiệu tuỳ chỉnh mới cho một mã thông tin xác thực cụ thể và duy trì nội dung cập nhật trên máy chủ.
- Xoá thông tin xác thực: Xoá một mã thông tin xác thực cụ thể khỏi cơ sở dữ liệu.
- Cập nhật thông tin chi tiết về tài khoản người dùng: Chấp nhận các giá trị
name(tên người dùng) vàdisplayNameđã cập nhật cho hồ sơ của người dùng đã đăng nhập để ứng dụng có thể đồng bộ hoá các giá trị đó với nhà cung cấp khoá truy cập.
// Node.js routing example for credential CRUD and user profile rename
router.get('/api/credentials', checkUserAuthenticated, async (req, res) => {
const list = await db.findCredentialsByUserId(req.user.id);
return res.json(list);
});
router.put('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
const { id } = req.params;
const { name } = req.body;
const cred = await db.findCredentialById(id);
if (!cred || cred.passkeyUserId !== req.user.id) {
return res.status(404).json({ error: 'Credential not found.' });
}
cred.name = name;
await db.saveCredential(cred);
return res.json(cred);
});
router.delete('/api/credential/:id', checkUserAuthenticated, async (req, res) => {
const { id } = req.params;
const cred = await db.findCredentialById(id);
if (!cred || cred.passkeyUserId !== req.user.id) {
return res.status(404).json({ error: 'Credential not found.' });
}
await db.deleteCredential(id);
return res.json({ success: true });
});
router.put('/api/user', checkUserAuthenticated, async (req, res) => {
const { name, displayName } = req.body;
const updatedUser = await db.updateUser(req.user.id, { name, displayName });
return res.json(updatedUser);
});
Giao diện người dùng quản lý phía máy khách
Hiển thị một bảng điều khiển chế độ cài đặt riêng, cho phép người dùng dễ dàng kiểm tra và quản lý các lựa chọn xác thực đã đăng ký và thông tin chi tiết về hồ sơ tài khoản:
- Hiển thị danh sách đã lưu: Tìm nạp danh sách từ điểm cuối và hiển thị từng hàng thông tin xác thực. Nếu phản hồi trống, hãy hiển thị thông báo trạng thái trống hữu ích (ví dụ: "Không tìm thấy khoá truy cập").
- Ánh xạ siêu dữ liệu AAGUID: Đối với mỗi khoá truy cập, hãy tra cứu thuộc tính
aaguidcủa khoá đó trong sổ đăng ký cục bộ để hiển thị thông tin chi tiết về nhà cung cấp. Hãy xem phần Xác định nhà cung cấp khoá truy cập từ AAGUID để biết thêm thông tin chi tiết. - Yêu cầu về giao diện người dùng cho mỗi mục: Mỗi hàng bên trong vùng chứa danh sách PHẢI hiển thị:
- Biểu tượng nhà cung cấp: Hình ảnh hoặc URI dữ liệu bắt nguồn từ AAGUID.
- Nhà cung cấp/Tên tuỳ chỉnh: Tên bắt nguồn từ AAGUID hoặc chuỗi do người dùng đổi tên.
- Registration Date (Ngày đăng ký): Dấu thời gian bắt đầu thô được lưu trữ trong cơ sở dữ liệu
registeredAtđược định dạng thành ngày mà con người có thể đọc được để hiển thị cho ứng dụng. - Ngày sử dụng gần đây nhất: Dấu thời gian bắt đầu kỷ nguyên thô được lưu trữ trong cơ sở dữ liệu
lastUsedAtđược định dạng thành ngày mà con người có thể đọc được (nếu có) để hiển thị cho ứng dụng. - Nút đổi tên: Kích hoạt thao tác đổi tên để cập nhật biệt hiệu tuỳ chỉnh của thông tin xác thực trên máy chủ (
PUT /api/credential/:id). Lưu ý: Việc đổi tên biệt hiệu của một khoá truy cập riêng lẻ là thay đổi nhãn phía máy chủ và KHÔNG gọisignalCurrentUserDetails(). - Nút xoá: Kích hoạt thao tác xoá thông tin xác thực (
DELETE /api/credential/:id) và đồng bộ hoá các mã thông tin xác thực còn lại thông quasignalAllAcceptedCredentials().
- Giao diện người dùng đổi tên tài khoản người dùng:
- Cung cấp một chế độ kiểm soát cho phép người dùng cập nhật tên người dùng của tài khoản (
name) hoặcdisplayName(PUT /api/user) và đồng bộ hoá ngay thông tin chi tiết đã cập nhật của người dùng với trình quản lý mật khẩu thông quaPublicKeyCredential.signalCurrentUserDetails().
- Cung cấp một chế độ kiểm soát cho phép người dùng cập nhật tên người dùng của tài khoản (
- Nút "Tạo khoá truy cập" có điều kiện:
- Cung cấp nút kích hoạt đăng ký "Tạo khoá truy cập" nổi bật trên trang quản lý. Trước khi hiển thị phần tử trên giao diện người dùng này, trang PHẢI phát hiện khả năng bằng cách sử dụng
PublicKeyCredential.getClientCapabilities()để xác minh rằng trình xác thực nền tảng được hỗ trợ. Nếu khoá truy cập không được hỗ trợ, hãy ẩn nút này và khuyến khích người dùng đăng ký MFA tiêu chuẩn. - Cho phép đăng ký khoá bảo mật bằng cách bỏ qua
authenticatorSelection.authenticatorAttachmenttrong lệnh gọinavigator.credentials.create().
- Cung cấp nút kích hoạt đăng ký "Tạo khoá truy cập" nổi bật trên trang quản lý. Trước khi hiển thị phần tử trên giao diện người dùng này, trang PHẢI phát hiện khả năng bằng cách sử dụng
Đồng bộ hoá API tín hiệu
Signal API cho phép ứng dụng thông báo trạng thái thông tin xác thực cho trình quản lý mật khẩu, giúp kho lưu trữ đã đồng bộ hoá của người dùng và cơ sở dữ liệu phụ trợ của bạn luôn đồng bộ.
- Quy tắc phát hiện tính năng và mã hoá tham số:
- Luôn phát hiện tính năng của các phương thức Signal API trước khi gọi (
if (PublicKeyCredential.signalAllAcceptedCredentials)vàif (PublicKeyCredential.signalCurrentUserDetails)). - Tất cả tham số
userIdvà mã thông tin xác thực được truyền đến các phương thức Signal API (signalAllAcceptedCredentials,signalCurrentUserDetails) PHẢI là chuỗi được mã hoá Base64URL.
- Luôn phát hiện tính năng của các phương thức Signal API trước khi gọi (
- Bắt đầu đồng bộ hoá khi tải trang:
- Ứng dụng PHẢI tự động gọi
signalAllAcceptedCredentials()khi trang quản lý tải (ví dụ: bên trong trình nghe sự kiệnDOMContentLoaded, khởi chạy mô-đun hoặc hook gắn thành phần khung nhưuseEffect).
- Ứng dụng PHẢI tự động gọi
- Đồng bộ hoá thông tin cập nhật về quản lý:
- Ứng dụng PHẢI gọi
signalAllAcceptedCredentials()ngay lập tức trong trình xử lý lượt nhấp xoá thông tin xác thực sau khi tìm nạp. - Ứng dụng PHẢI gọi
signalCurrentUserDetails()ngay lập tức trong trình xử lý lượt nhấp đổi tên tài khoản người dùng (namehoặcdisplayName) sau khi tìm nạp.
- Ứng dụng PHẢI gọi
// Client-side management synchronization ES module
import { listFetch, renameCredentialFetch, updateUserFetch, deleteFetch } from './api.js';
// Base64URL-encoded User ID string (illustration only)
const base64UrlUserId = "M2YPl-KGnA8";
async function syncAcceptedCredentials(currentCredentialsList) {
if (!window.PublicKeyCredential || !PublicKeyCredential.signalAllAcceptedCredentials) return;
try {
const credentialIds = currentCredentialsList.map(c => c.id); // Array of Base64URL credential ID strings
await PublicKeyCredential.signalAllAcceptedCredentials({
rpId, // RP ID must match the one defined on the server
userId: base64UrlUserId, // User ID Base64URL-encoded string
allAcceptedCredentialIds: credentialIds
});
} catch (e) {
console.error('SignalAllAcceptedCredentials sync failure:', e);
}
}
async function loadManagementPanel() {
const response = await listFetch();
const list = await response.json();
renderUI(list);
// Sync accepted credentials on initial load
await syncAcceptedCredentials(list);
}
async function performDelete(credentialId) {
const response = await deleteFetch(credentialId);
if (response.ok) {
const updatedResponse = await listFetch();
const updatedList = await updatedResponse.json();
renderUI(updatedList);
// Sync remaining accepted credentials after deletion
await syncAcceptedCredentials(updatedList);
}
}
// Renaming a passkey's nickname updates the server record only
async function performCredentialRename(credentialId, newCredentialName) {
const response = await renameCredentialFetch(credentialId, { name: newCredentialName });
if (response.ok) {
const updatedResponse = await listFetch();
renderUI(await updatedResponse.json());
}
}
// Renaming the user's account username or displayName triggers signalCurrentUserDetails
async function performUserRename(rpId, userId, updatedName, updatedDisplayName) {
const response = await updateUserFetch({ name: updatedName, displayName: updatedDisplayName });
if (response.ok && window.PublicKeyCredential && PublicKeyCredential.signalCurrentUserDetails) {
try {
await PublicKeyCredential.signalCurrentUserDetails({
rpId, // RP ID must match the one defined on the server
userId, // Base64URL-encoded user ID
name: updatedName, // Updated username
displayName: updatedDisplayName // Updated display name
});
} catch (e) {
console.error('SignalCurrentUserDetails sync failure:', e);
}
}
}
window.addEventListener('DOMContentLoaded', loadManagementPanel);
Xác định nhà cung cấp khoá truy cập từ AAGUID
AAGUID (Mã nhận dạng duy nhất trên toàn cầu của chứng thực trình xác thực) là một giá trị nhận dạng riêng biệt 128 bit đại diện cho mẫu trình xác thực, chứ không phải một thực thể cụ thể. AAGUID có trong dữ liệu của trình xác thực trong quá trình đăng ký khoá truy cập và có thể dùng để xác định nhà cung cấp khoá truy cập nào (ví dụ: Trình quản lý mật khẩu của Google, Chuỗi khoá iCloud, 1Password) đã tạo thông tin xác thực.
Bạn chỉ nên dùng AAGUID để giúp người dùng quản lý khoá truy cập. Bạn có thể sửa đổi khoá truy cập trừ phi khoá truy cập đó được chứng thực bằng mật mã, điều mà khoá truy cập nền tảng hiện không hỗ trợ.
1. Sổ đăng ký AAGUID
Bạn có thể xem thông tin liên kết JSON do cộng đồng duy trì giữa AAGUID với tên và biểu tượng của nhà cung cấp tại:
https://raw.githubusercontent.com/passkeydeveloper/passkey-authenticator-aaguids/refs/heads/main/combined_aaguid.json
Mỗi mục nhập có giản đồ sau:
{
"<aaguid-uuid>": {
"name": "Provider Name",
"icon_light": "data:image/png;base64,...",
"icon_dark": "data:image/png;base64,..."
}
}
2. Sử dụng AAGUID sau khi đăng ký
Sau khi xác minh phản hồi đăng ký, hãy đọc aaguid từ kết quả đăng ký và tra cứu trong sổ đăng ký để điền name và providerIcon của thông tin xác thực:
Trước khi tra cứu AAGUID trong sổ đăng ký, hãy kiểm tra xem giá trị này có bằng '00000000-0000-0000-0000-000000000000' hay không. Nếu có, hãy bỏ qua bước tra cứu sổ đăng ký và đặt name thành một giá trị dự phòng (ví dụ: tên thiết bị từ tác nhân người dùng hoặc "Nhà cung cấp khoá truy cập không xác định") và đặt providerIcon thành undefined. Chỉ tra cứu sổ đăng ký cho các AAGUID khác 0.
import aaguids from './aaguids.json' with { type: 'json' };
const { aaguid } = registrationInfo;
if (aaguid === '00000000-0000-0000-0000-000000000000') {
// use the device name as the passkey provider based on
// the information derived from the user agent string,
// or just say "Unknown passkey provider"
} else {
const provider = aaguids[aaguid];
const credential = {
// ...other fields
aaguid,
name: provider?.name || 'Unknown passkey provider',
providerIcon: provider?.icon_light,
};
}
Chiến lược dự phòng
Dự phòng phát hiện tính năng khoá truy cập
Luôn cài đặt "webauthn-polyfills" và nhập vào ngữ cảnh.
Hãy cân nhắc rằng miễn là PublicKeyCredential được hỗ trợ, thì PublicKeyCredential.getClientCapabilities cũng được hỗ trợ.
import 'webauthn-polyfills';
Dự phòng đồng bộ hoá API tín hiệu
Nếu trình duyệt không hỗ trợ PublicKeyCredential.parseRequestOptionsFromJSON, hãy sử dụng "webauthn-polyfills":
<script type="module">
if (!PublicKeyCredential.parseRequestOptionsFromJSON) {
await import('https://unpkg.com/webauthn-polyfills');
}
</script>
Thao tác này cũng sẽ thêm tính năng hỗ trợ cho PublicKeyCredential.prototype.toJSON.