Hướng dẫn này giải thích cách triển khai passkey trong ứng dụng web của bạn. Chúng tôi sử dụng Node.js (TypeScript), SimpleWebAuthn, Vanilla HTML / JavaScript và MySQL.
Vincent
Created: June 20, 2025
Updated: June 20, 2025
We aim to make the Internet a safer place using passkeys. That's why we want to support developers with tutorials on how to implement passkeys.
Trong hướng dẫn này, chúng tôi sẽ hỗ trợ bạn trong nỗ lực triển khai passkey, cung cấp hướng dẫn từng bước về cách thêm passkey vào trang web của bạn.
Việc có một hệ thống xác thực hiện đại, mạnh mẽ và thân thiện với người dùng là yếu tố then chốt khi bạn muốn xây dựng một trang web hoặc ứng dụng tuyệt vời. Passkey đã nổi lên như một giải pháp cho thách thức này. Đóng vai trò là tiêu chuẩn mới cho việc đăng nhập, chúng hứa hẹn một tương lai không còn những nhược điểm của mật khẩu truyền thống, mang lại trải nghiệm đăng nhập thực sự không cần mật khẩu (không chỉ an toàn mà còn rất tiện lợi).
Recent Articles
Điều thực sự thể hiện tiềm năng của passkey là sự ủng hộ mà chúng đã nhận được. Mọi trình duyệt lớn như Chrome, Firefox, Safari, hay Edge và tất cả các nhà sản xuất thiết bị quan trọng (Apple, Microsoft, Google) đều đã tích hợp hỗ trợ. Sự đón nhận đồng lòng này cho thấy passkey chính là tiêu chuẩn mới cho việc đăng nhập.
Đúng vậy, đã có những hướng dẫn về việc tích hợp passkey vào các ứng dụng web. Dù là cho các framework frontend như React, Vue.js hay Next.js, có rất nhiều hướng dẫn được thiết kế để giảm thiểu thách thức và tăng tốc quá trình triển khai passkey của bạn. Tuy nhiên, một hướng dẫn end-to-end mà vẫn tối giản và cơ bản thì lại thiếu. Nhiều nhà phát triển đã liên hệ với chúng tôi và yêu cầu một hướng dẫn làm rõ việc triển khai passkey cho các ứng dụng web.
Đây chính là lý do tại sao chúng tôi đã tạo ra hướng dẫn này. Mục tiêu của chúng tôi? Tạo ra một thiết lập tối thiểu khả thi cho passkey, bao gồm lớp frontend, backend và cơ sở dữ liệu (lớp sau cùng thường bị bỏ qua mặc dù nó có thể gây ra một số vấn đề đau đầu nghiêm trọng).
Khi kết thúc hành trình này, bạn sẽ xây dựng được một ứng dụng web tối thiểu khả thi, nơi bạn có thể:
Đối với những người vội vàng hoặc muốn có tài liệu tham khảo, toàn bộ mã nguồn có sẵn trên GitHub.
Tò mò về kết quả cuối cùng trông như thế nào? Đây là một cái nhìn sơ lược về dự án cuối cùng (chúng tôi thừa nhận nó trông rất cơ bản nhưng những điều thú vị nằm ở bên dưới):
Chúng tôi hoàn toàn nhận thức được rằng các phần của mã nguồn và dự án có thể được thực hiện theo cách khác hoặc tinh vi hơn nhưng chúng tôi muốn tập trung vào những điều cốt lõi. Đó là lý do tại sao chúng tôi cố tình giữ mọi thứ đơn giản và tập trung vào passkey.
Làm thế nào để thêm passkey vào trang web sản phẩm của tôi?
Đây là một ví dụ rất tối thiểu về xác thực bằng passkey. Những điều sau đây KHÔNG được xem xét / triển khai trong hướng dẫn này hoặc chỉ ở mức rất cơ bản:
Để có được sự hỗ trợ đầy đủ cho tất cả các tính năng này đòi hỏi nỗ lực phát triển lớn hơn rất nhiều. Đối với những người quan tâm, chúng tôi khuyên bạn nên xem bài viết về những quan niệm sai lầm của nhà phát triển passkey này.
Trước khi đi sâu vào việc triển khai passkey, hãy cùng xem xét các kỹ năng và công cụ cần thiết. Đây là những gì bạn cần để bắt đầu:
Kiến thức vững chắc về các thành phần xây dựng web như HTML, CSS và JavaScript là điều cần thiết. Chúng tôi đã cố tình giữ mọi thứ đơn giản, không sử dụng bất kỳ framework JavaScript hiện đại nào và dựa vào Vanilla JavaScript / HTML. Điều phức tạp hơn duy nhất chúng tôi sử dụng là thư viện bao bọc WebAuthn @simplewebauthn/browser.
Đối với backend của chúng tôi, chúng tôi sử dụng một máy chủ
Node.js (Express) được viết bằng TypeScript. Chúng tôi cũng đã
quyết định làm việc với triển khai máy chủ WebAuthn của
SimpleWebAuthn
(@simplewebauthn/server
cùng với @simplewebauthn/typescript-types
). Có rất nhiều triển
khai máy chủ WebAuthn có sẵn, vì vậy bạn tất nhiên cũng có thể sử dụng bất kỳ cái nào
trong số đó. Vì chúng tôi đã quyết định chọn máy chủ WebAuthn TypeScript, kiến thức cơ bản
về Node.js và npm là bắt buộc.
Tất cả dữ liệu người dùng và khóa công khai của passkey được lưu trữ trong cơ sở dữ liệu. Chúng tôi đã chọn MySQL làm công nghệ cơ sở dữ liệu. Hiểu biết cơ bản về MySQL và cơ sở dữ liệu quan hệ là một lợi thế, mặc dù chúng tôi sẽ hướng dẫn bạn qua từng bước một.
Trong phần tiếp theo, chúng tôi thường sử dụng các thuật ngữ WebAuthn và passkey thay thế cho nhau mặc dù chúng có thể không chính thức có cùng ý nghĩa. Tuy nhiên, để dễ hiểu hơn, đặc biệt là trong phần mã nguồn, chúng tôi đưa ra giả định này.
Với những điều kiện tiên quyết này, bạn đã sẵn sàng để bước vào thế giới của passkey.
Ben Gould
Head of Engineering
I’ve built hundreds of integrations in my time, including quite a few with identity providers and I’ve never been so impressed with a developer experience as I have been with Corbado.
Hơn 10.000 nhà phát triển tin tưởng Corbado và làm cho Internet an toàn hơn với passkey. Có câu hỏi? Chúng tôi đã viết hơn 150 bài blog về passkey.
Tham gia Cộng đồng PasskeyTrước khi đi vào mã nguồn và cấu hình, hãy cùng xem qua kiến trúc của hệ thống mà chúng ta muốn xây dựng. Dưới đây là phân tích về kiến trúc mà chúng ta sẽ thiết lập:
Với tổng quan kiến trúc này, bạn sẽ có một bản đồ khái niệm về cách các thành phần của ứng dụng của chúng ta. Khi chúng ta tiếp tục, chúng ta sẽ đi sâu hơn vào từng thành phần này, chi tiết về thiết lập, cấu hình và sự tương tác của chúng.
Sơ đồ sau mô tả luồng quy trình trong quá trình đăng ký (sign-up):
Sơ đồ sau mô tả luồng quy trình trong quá trình xác thực (đăng nhập):
Ngoài ra, bạn có thể tìm thấy cấu trúc dự án ở đây (chỉ những tệp quan trọng nhất):
passkeys-tutorial ├── src # Chứa tất cả mã nguồn TypeScript của backend │ ├── controllers # Logic nghiệp vụ để xử lý các loại yêu cầu cụ thể │ │ ├── authentication.ts # Logic xác thực Passkey │ │ └── registration.ts # Logic đăng ký Passkey │ ├── middleware │ │ ├── customError.ts # Thêm thông báo lỗi tùy chỉnh theo cách chuẩn hóa │ │ └── errorHandler.ts # Trình xử lý lỗi chung │ ├── public │ │ ├── index.html # Tệp HTML chính ở frontend │ │ ├── css │ │ │ └── style.css # Định dạng cơ bản │ │ └── js │ │ └── script.js # Logic JavaScript (bao gồm API WebAuthn) │ ├── routes # Định nghĩa các route API và trình xử lý của chúng │ │ └── routes.ts # Các route passkey cụ thể │ ├── services │ │ ├── credentialService.ts# Tương tác với bảng credential │ │ └── userService.ts # Tương tác với bảng user │ ├── utils # Các hàm và tiện ích trợ giúp │ | ├── constants.ts # Một số hằng số (ví dụ: rpID) │ | └── utils.ts # Hàm trợ giúp │ ├── database.ts # Tạo kết nối từ Node.js đến cơ sở dữ liệu MySQL │ ├── index.ts # Điểm vào của máy chủ Node.js │ └── server.ts # Quản lý tất cả các cài đặt máy chủ ├── config.json # Một số cấu hình cho dự án Node.js ├── docker-compose.yml # Định nghĩa các dịch vụ, mạng và volume cho các container Docker ├── Dockerfile # Tạo một image Docker của dự án ├── init-db.sql # Định nghĩa lược đồ cơ sở dữ liệu MySQL của chúng tôi ├── package.json # Quản lý các phụ thuộc và script của dự án Node.js └── tsconfig.json # Cấu hình cách TypeScript biên dịch mã của bạn
Khi triển khai passkey, việc thiết lập cơ sở dữ liệu là một thành phần quan trọng. Cách tiếp cận của chúng tôi sử dụng một container Docker chạy MySQL, cung cấp một môi trường đơn giản và cô lập, cần thiết cho việc kiểm thử và triển khai đáng tin cậy.
Lược đồ cơ sở dữ liệu của chúng tôi được cố tình giữ ở mức tối giản, chỉ có hai bảng. Sự đơn giản này giúp hiểu rõ hơn và bảo trì dễ dàng hơn.
Cấu trúc bảng chi tiết
1. Bảng Credentials: Là trung tâm của việc xác thực bằng passkey, bảng này lưu trữ thông tin xác thực passkey. Các cột quan trọng:
credential_id
, kiểu dữ liệu và định dạng phù hợp là rất quan trọng.2. Bảng Users: Liên kết tài khoản người dùng với các thông tin xác thực tương ứng của họ.
Lưu ý rằng chúng tôi đã đặt tên bảng đầu tiên là credentials vì theo kinh nghiệm của chúng tôi và những gì các thư viện khác đề xuất, nó phù hợp hơn (trái với đề xuất của SimpleWebAuthn là đặt tên nó là authenticator hoặc authenticator_device).
Các kiểu dữ liệu cho credential_id
và public_key
là rất quan trọng. Lỗi thường phát
sinh từ các kiểu dữ liệu, mã hóa hoặc định dạng không chính xác (đặc biệt là sự khác biệt
giữa Base64 và Base64URL là một nguyên nhân phổ biến gây ra lỗi), có thể làm gián đoạn
toàn bộ quá trình đăng ký (sign-up) hoặc xác thực (login).
Tất cả các lệnh SQL cần thiết để thiết lập các bảng này đều nằm trong tệp init-db.sql
.
Script này đảm bảo việc khởi tạo cơ sở dữ liệu nhanh chóng và không có lỗi.
Đối với các trường hợp phức tạp hơn, bạn có thể thêm credential_device_type
hoặc
credential_backed_up
để lưu trữ thêm thông tin về thông tin xác thực và cải thiện trải
nghiệm người dùng. Tuy nhiên, chúng tôi sẽ không làm điều đó trong hướng dẫn này.
init-db.sqlCREATE TABLE users ( id VARCHAR(255) PRIMARY KEY, username VARCHAR(255) NOT NULL UNIQUE ); CREATE TABLE credentials ( id INT AUTO_INCREMENT PRIMARY KEY, user_id VARCHAR(255) NOT NULL, credential_id VARCHAR(255) NOT NULL, public_key TEXT NOT NULL, counter INT NOT NULL, transports VARCHAR(255), FOREIGN KEY (user_id) REFERENCES users (id) );
Sau khi chúng ta đã tạo tệp này, chúng ta tạo một tệp docker-compose.yml
mới ở cấp gốc
của dự án:
docker-compose.ymlversion: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql
Tệp này khởi động cơ sở dữ liệu MySQL trên cổng 3306 và tạo cấu trúc cơ sở dữ liệu đã định nghĩa. Điều quan trọng cần lưu ý là tên và mật khẩu cho cơ sở dữ liệu được sử dụng ở đây được giữ đơn giản cho mục đích minh họa. Trong môi trường sản phẩm, bạn nên sử dụng thông tin xác thực phức tạp hơn để tăng cường bảo mật.
Tiếp theo, chúng ta chuyển sang chạy container Docker của mình. Tại thời điểm này, tệp
docker-compose.yml
của chúng ta chỉ bao gồm container duy nhất này, nhưng chúng ta sẽ
thêm nhiều thành phần hơn sau. Để khởi động container, sử dụng lệnh sau:
docker compose up -d
Khi container đã hoạt động, chúng ta cần xác minh xem cơ sở dữ liệu có hoạt động như mong đợi không. Mở một terminal và thực thi lệnh sau để tương tác với cơ sở dữ liệu MySQL:
docker exec -it <container ID> mysql -uroot -p
Bạn sẽ được yêu cầu nhập mật khẩu root, trong ví dụ của chúng tôi là my-secret-pw
. Sau
khi đăng nhập, chọn cơ sở dữ liệu webauthn_db
và hiển thị các bảng bằng các lệnh sau:
use webauthn_db; show tables;
Ở giai đoạn này, bạn sẽ thấy hai bảng được định nghĩa trong script của chúng tôi. Ban đầu, các bảng này sẽ trống, cho thấy rằng việc thiết lập cơ sở dữ liệu của chúng ta đã hoàn tất và sẵn sàng cho các bước tiếp theo trong việc triển khai passkey.
Backend là cốt lõi của bất kỳ ứng dụng passkey nào, hoạt động như một trung tâm xử lý các yêu cầu xác thực người dùng từ frontend. Nó giao tiếp với thư viện máy chủ WebAuthn để xử lý các yêu cầu đăng ký (sign-up) và xác thực (login), và nó tương tác với cơ sở dữ liệu MySQL của bạn để lưu trữ và truy xuất thông tin xác thực người dùng. Dưới đây, chúng tôi sẽ hướng dẫn bạn thiết lập backend bằng Node.js (Express) với TypeScript, sẽ cung cấp một API công khai để xử lý tất cả các yêu cầu.
Đầu tiên, tạo một thư mục mới cho dự án của bạn và điều hướng vào đó bằng terminal hoặc dấu nhắc lệnh của bạn.
Chạy lệnh
npx create-express-typescript-application passkeys-tutorial
Điều này tạo ra một bộ khung mã cơ bản của một ứng dụng Node.js (Express) được viết bằng TypeScript mà chúng ta có thể sử dụng để điều chỉnh thêm.
Dự án của bạn yêu cầu một số gói chính mà chúng ta cần cài đặt thêm:
Chuyển vào thư mục mới và cài đặt chúng bằng các lệnh sau (chúng tôi cũng cài đặt các loại TypeScript cần thiết):
cd passkeys-tutorial npm install @simplewebauthn/server mysql2 uuid express-session @types/express-session @types/uuid
Để xác nhận rằng mọi thứ đã được cài đặt chính xác, hãy chạy
npm run dev:nodemon
Điều này sẽ khởi động máy chủ Node.js của bạn ở chế độ phát triển với Nodemon, tự động khởi động lại máy chủ khi có bất kỳ thay đổi tệp nào.
Mẹo khắc phục sự cố: Nếu bạn gặp lỗi, hãy thử cập nhật ts-node
lên phiên bản 10.8.1
trong tệp package.json
và sau đó chạy npm i
để cài đặt các bản cập nhật.
Tệp server.ts
của bạn có thiết lập cơ bản và middleware cho một ứng dụng
Express. Để tích hợp chức năng passkey, bạn sẽ cần thêm:
Những cải tiến này là chìa khóa để kích hoạt xác thực bằng passkey trong backend của ứng dụng của bạn. Chúng ta sẽ thiết lập chúng sau.
Sau khi chúng ta đã tạo và khởi động cơ sở dữ liệu trong
phần 4, bây giờ chúng ta cần đảm bảo rằng backend của
chúng ta có thể kết nối với cơ sở dữ liệu MySQL. Do đó, chúng ta tạo một tệp database.ts
mới trong thư mục /src
và thêm nội dung sau:
database.tsimport mysql from "mysql2"; // Create a MySQL pool const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0, }); // Promisify for Node.js async/await. export const promisePool = pool.promise();
Tệp này sẽ được máy chủ của chúng ta sử dụng sau này để truy cập cơ sở dữ liệu.
Hãy xem qua tệp config.json
của chúng ta, nơi hai biến đã được định nghĩa: cổng mà chúng
ta chạy ứng dụng và môi trường:
config.json{ "PORT": 8080, "NODE_ENV": "development" }
package.json
có thể giữ nguyên và sẽ trông như sau:
package.json{ "name": "passkeys-tutorial", "version": "0.0.1", "description": "passkeys-tutorial initialised with create-express-typescript-application.", "main": "src/index.ts", "scripts": { "build": "tsc", "start": "node ./build/src/index.js", "dev": "ts-node ./src/index.ts", "dev:nodemon": "nodemon -w src -e ts,json -x ts-node ./src/index.ts", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": ["express", "typescript"], "devDependencies": { "@types/cors": "^2.8.17", "@types/express": "^4.17.21", "@types/morgan": "^1.9.9", "@types/node": "^14.18.63", "@typescript-eslint/eslint-plugin": "^4.33.0", "@typescript-eslint/parser": "^4.33.0", "eslint": "^7.32.0", "nodemon": "^2.0.22", "ts-node": "^10.8.1", "typescript": "^4.9.5" }, "dependencies": { "@simplewebauthn/server": "^8.3.5", "@types/express-session": "^1.17.10", "@types/uuid": "^9.0.7", "cors": "^2.8.5", "env-cmd": "^10.1.0", "express": "^4.18.2", "express-session": "^1.17.3", "fs": "^0.0.1-security", "helmet": "^4.6.0", "morgan": "^1.10.0", "mysql2": "^3.6.5", "uuid": "^9.0.1" } }
index.ts
trông như sau:
index.tsimport app from "./server"; import config from "../config.json"; // Start the application by listening to specific port const port = Number(process.env.PORT || config.PORT || 8080); app.listen(port, () => { console.info("Express application started on port: " + port); });
Trong server.ts
, chúng ta cần điều chỉnh thêm một số thứ. Hơn nữa, cần có một bộ đệm tạm
thời nào đó (ví dụ: redis, memcache hoặc express-session) để lưu
trữ các challenge tạm thời mà người dùng có thể xác thực. Chúng tôi quyết định sử dụng
express-session
và khai báo module express-session
ở trên cùng để mọi thứ hoạt động
với express-session
. Ngoài ra, chúng tôi sắp xếp hợp lý việc định tuyến và loại bỏ việc
xử lý lỗi hiện tại (điều này sẽ được thêm vào middleware sau):
server.tsimport express, { Express } from "express"; import morgan from "morgan"; import helmet from "helmet"; import cors from "cors"; import config from "../config.json"; import { router as passkeyRoutes } from "./routes/routes"; import session from "express-session"; const app: Express = express(); declare module "express-session" { interface SessionData { currentChallenge?: string; loggedInUserId?: string; } } /************************************************************************************ * Basic Express Middlewares ***********************************************************************************/ app.set("json spaces", 4); app.use(express.json()); app.use(express.urlencoded({ extended: true })); app.use( session({ // @ts-ignore secret: process.env.SESSION_SECRET, saveUninitialized: true, resave: false, cookie: { maxAge: 86400000, httpOnly: true, // Ensure to not expose session cookies to clientside scripts }, }), ); // Handle logs in console during development if (process.env.NODE_ENV === "development" || config.NODE_ENV === "development") { app.use(morgan("dev")); app.use(cors()); } // Handle security and origin in production if (process.env.NODE_ENV === "production" || config.NODE_ENV === "production") { app.use(helmet()); } /************************************************************************************ * Register all routes ***********************************************************************************/ app.use("/api/passkey", passkeyRoutes); app.use(express.static("src/public")); export default app;
Để quản lý hiệu quả dữ liệu trong hai bảng đã tạo của chúng ta, chúng ta sẽ phát triển hai
service riêng biệt trong một thư mục src/services
mới: authenticatorService.ts
và
userService.ts
.
Mỗi service sẽ đóng gói các phương thức CRUD (Create, Read, Update, Delete), cho phép chúng ta tương tác với cơ sở dữ liệu một cách mô-đun và có tổ chức. Các service này sẽ tạo điều kiện thuận lợi cho việc lưu trữ, truy xuất và cập nhật dữ liệu trong các bảng authenticator và user. Dưới đây là cách bố trí cấu trúc của các tệp cần thiết này:
userService.ts
trông như thế này:
userService.tsimport { promisePool } from "../database"; // Adjust the import path as necessary import { v4 as uuidv4 } from "uuid"; export const userService = { async getUserById(userId: string) { const [rows] = await promisePool.query("SELECT * FROM users WHERE id = ?", [ userId, ]); // @ts-ignore return rows[0]; }, async getUserByUsername(username: string) { try { const [rows] = await promisePool.query( "SELECT * FROM users WHERE username = ?", [username], ); // @ts-ignore return rows[0]; } catch (error) { return null; } }, async createUser(username: string) { const id = uuidv4(); await promisePool.query("INSERT INTO users (id, username) VALUES (?, ?)", [ id, username, ]); return { id, username }; }, };
credentialService.ts
trông như sau:
credentialService.tsimport { promisePool } from "../database"; import type { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; export const credentialService = { async saveNewCredential( userId: string, credentialId: string, publicKey: string, counter: number, transports: string, ) { try { await promisePool.query( "INSERT INTO credentials (user_id, credential_id, public_key, counter, transports) VALUES (?, ?, ?, ?, ?)", [userId, credentialId, publicKey, counter, transports], ); } catch (error) { console.error("Error saving new credential:", error); throw error; } }, async getCredentialByCredentialId( credentialId: string, ): Promise<AuthenticatorDevice | null> { try { const [rows] = await promisePool.query( "SELECT * FROM credentials WHERE credential_id = ? LIMIT 1", [credentialId], ); // @ts-ignore if (rows.length === 0) return null; // @ts-ignore const row = rows[0]; return { userID: row.user_id, credentialID: row.credential_id, credentialPublicKey: row.public_key, counter: row.counter, transports: row.transports ? row.transports.split(",") : [], } as AuthenticatorDevice; } catch (error) { console.error("Error retrieving credential:", error); throw error; } }, async updateCredentialCounter(credentialId: string, newCounter: number) { try { await promisePool.query( "UPDATE credentials SET counter = ? WHERE credential_id = ?", [newCounter, credentialId], ); } catch (error) { console.error("Error updating credential counter:", error); throw error; } }, };
Để xử lý lỗi một cách tập trung và cũng giúp việc gỡ lỗi dễ dàng hơn, chúng tôi thêm một
tệp errorHandler.ts
:
errorHandler.tsimport { Request, Response, NextFunction } from "express"; import { CustomError } from "./customError"; interface ErrorWithStatus extends Error { statusCode?: number; } export const handleError = ( err: CustomError, req: Request, res: Response, next: NextFunction, ) => { const statusCode = err.statusCode || 500; const message = err.message || "Internal Server Error"; console.log(message); res.status(statusCode).send({ error: message }); };
Bên cạnh đó, chúng tôi thêm một tệp customError.ts
mới vì sau này chúng tôi muốn có thể
tạo ra các lỗi tùy chỉnh để giúp chúng tôi tìm ra lỗi nhanh hơn:
customError.tsexport class CustomError extends Error { statusCode: number; constructor(message: string, statusCode: number = 500) { super(message); this.statusCode = statusCode; Object.setPrototypeOf(this, CustomError.prototype); } }
Trong thư mục utils
, chúng tôi tạo hai tệp constants.ts
và utils.ts
.
constant.ts
chứa một số thông tin cơ bản của máy chủ WebAuthn, như tên
relying party, ID relying party và
origin:
constant.tsexport const rpName: string = "Passkeys Tutorial"; export const rpID: string = "localhost"; export const origin: string = `http://${rpID}:8080`;
utils.ts
chứa hai hàm mà chúng ta sẽ cần sau này để mã hóa và giải mã dữ liệu:
utils.tsexport const uint8ArrayToBase64 = (uint8Array: Uint8Array): string => Buffer.from(uint8Array).toString("base64"); export const base64ToUint8Array = (base64: string): Uint8Array => new Uint8Array(Buffer.from(base64, "base64"));
Bây giờ, chúng ta đến với phần cốt lõi của backend: các controller. Chúng tôi tạo hai
controller, một để tạo passkey mới (registration.ts
) và một để đăng nhập bằng passkey
(authentication.ts
).
registration.ts
trông như thế này:
registration.tsimport { generateRegistrationOptions, verifyRegistrationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64 } from "../utils/utils"; import { rpName, rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { RegistrationResponseJSON } from "@simplewebauthn/typescript-types"; import { Request, Response, NextFunction } from "express"; import { CustomError } from "../middleware/customError"; export const handleRegisterStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; if (!username) { return next(new CustomError("Username empty", 400)); } try { let user = await userService.getUserByUsername(username); if (user) { return next(new CustomError("User already exists", 400)); } else { user = await userService.createUser(username); } const options = await generateRegistrationOptions({ rpName, rpID, userID: user.id, userName: user.username, timeout: 60000, attestationType: "direct", excludeCredentials: [], authenticatorSelection: { residentKey: "preferred", }, // Support for the two most common algorithms: ES256, and RS256 supportedAlgorithmIDs: [-7, -257], }); req.session.loggedInUserId = user.id; req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleRegisterFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const verification = await verifyRegistrationResponse({ response: body as RegistrationResponseJSON, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, requireUserVerification: true, }); if (verification.verified && verification.registrationInfo) { const { credentialPublicKey, credentialID, counter } = verification.registrationInfo; await credentialService.saveNewCredential( loggedInUserId, uint8ArrayToBase64(credentialID), uint8ArrayToBase64(credentialPublicKey), counter, body.response.transports, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.loggedInUserId = undefined; req.session.currentChallenge = undefined; } };
Hãy xem xét các chức năng của các controller của chúng ta, xử lý hai điểm cuối chính trong quy trình đăng ký (sign-up) WebAuthn. Đây cũng là nơi có một trong những khác biệt lớn nhất so với xác thực dựa trên mật khẩu: Đối với mỗi lần đăng ký (sign-up) hoặc xác thực (login), cần có hai lệnh gọi API backend, yêu cầu nội dung frontend cụ thể ở giữa. Mật khẩu thường chỉ cần một điểm cuối.
1. Điểm cuối handleRegisterStart:
Điểm cuối này được kích hoạt bởi frontend, nhận một tên người dùng để tạo một passkey và tài khoản mới. Trong ví dụ này, chúng tôi chỉ cho phép tạo tài khoản / passkey mới nếu chưa có tài khoản nào tồn tại. Trong các ứng dụng thực tế, bạn sẽ cần xử lý điều này theo cách thông báo cho người dùng rằng một passkey đã tồn tại và việc thêm từ cùng một thiết bị là không thể (nhưng người dùng có thể thêm passkey từ một thiết bị khác sau một hình thức xác nhận nào đó). Để đơn giản, chúng tôi bỏ qua điều này trong hướng dẫn này.
Các tùy chọn
PublicKeyCredentialCreationOptions được
chuẩn bị. residentKey
được đặt thành preferred
, và attestationType
thành direct
,
thu thập thêm dữ liệu từ authenticator để có thể lưu trữ trong
cơ sở dữ liệu.
Nói chung, PublicKeyCredentialCreationOptions bao gồm các dữ liệu sau:
dictionary PublicKeyCredentialCreationOptions { required PublicKeyCredentialRpEntity rp; required PublicKeyCredentialUserEntity user; required BufferSource challenge; required sequence<PublicKeyCredentialParameters> pubKeyCredParams; unsigned long timeout; sequence<PublicKeyCredentialDescriptor> excludeCredentials = []; AuthenticatorSelectionCriteria authenticatorSelection; DOMString attestation = "none"; AuthenticationExtensionsClientInputs extensions; };
rp.name
) và tên miền (rp.id
).user.name
, user.id
, và
user.displayName
.User ID và challenge được lưu trữ trong một đối tượng phiên, đơn giản hóa quy trình cho mục đích hướng dẫn. Hơn nữa, phiên được xóa sau mỗi lần đăng ký (sign-up) hoặc xác thực (login).
2. Điểm cuối handleRegisterFinish:
Điểm cuối này truy xuất user ID và challenge đã được
đặt trước đó. Nó xác minh RegistrationResponse
với challenge. Nếu hợp lệ, nó sẽ lưu trữ
một thông tin xác thực mới cho người dùng. Sau khi được lưu trữ trong cơ sở dữ liệu,
user ID và challenge sẽ bị xóa khỏi phiên.
Mẹo: Khi gỡ lỗi ứng dụng của bạn, chúng tôi thực sự khuyên bạn nên sử dụng Chrome làm trình duyệt và các tính năng tích hợp sẵn của nó để cải thiện trải nghiệm của nhà phát triển với các ứng dụng dựa trên passkey, ví dụ: authenticator WebAuthn ảo và nhật ký thiết bị (xem các mẹo của chúng tôi dành cho nhà phát triển bên dưới để biết thêm thông tin)
Tiếp theo, chúng ta chuyển sang authentication.ts
, có cấu trúc và chức năng tương tự.
authentication.ts
trông như thế này:
authentication.tsimport { Request, Response, NextFunction } from "express"; import { generateAuthenticationOptions, verifyAuthenticationResponse, } from "@simplewebauthn/server"; import { uint8ArrayToBase64, base64ToUint8Array } from "../utils/utils"; import { rpID, origin } from "../utils/constants"; import { credentialService } from "../services/credentialService"; import { userService } from "../services/userService"; import { AuthenticatorDevice } from "@simplewebauthn/typescript-types"; import { isoBase64URL } from "@simplewebauthn/server/helpers"; import { VerifiedAuthenticationResponse, VerifyAuthenticationResponseOpts, } from "@simplewebauthn/server/esm"; import { CustomError } from "../middleware/customError"; export const handleLoginStart = async ( req: Request, res: Response, next: NextFunction, ) => { const { username } = req.body; try { const user = await userService.getUserByUsername(username); if (!user) { return next(new CustomError("User not found", 404)); } req.session.loggedInUserId = user.id; // allowCredentials is purposely for this demo left empty. This causes all existing local credentials // to be displayed for the service instead only the ones the username has registered. const options = await generateAuthenticationOptions({ timeout: 60000, allowCredentials: [], userVerification: "required", rpID, }); req.session.currentChallenge = options.challenge; res.send(options); } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } }; export const handleLoginFinish = async ( req: Request, res: Response, next: NextFunction, ) => { const { body } = req; const { currentChallenge, loggedInUserId } = req.session; if (!loggedInUserId) { return next(new CustomError("User ID is missing", 400)); } if (!currentChallenge) { return next(new CustomError("Current challenge is missing", 400)); } try { const credentialID = isoBase64URL.toBase64(body.rawId); const bodyCredIDBuffer = isoBase64URL.toBuffer(body.rawId); const dbCredential: AuthenticatorDevice | null = await credentialService.getCredentialByCredentialId(credentialID); if (!dbCredential) { return next(new CustomError("Credential not registered with this site", 404)); } // @ts-ignore const user = await userService.getUserById(dbCredential.userID); if (!user) { return next(new CustomError("User not found", 404)); } // @ts-ignore dbCredential.credentialID = base64ToUint8Array(dbCredential.credentialID); // @ts-ignore dbCredential.credentialPublicKey = base64ToUint8Array( dbCredential.credentialPublicKey, ); let verification: VerifiedAuthenticationResponse; const opts: VerifyAuthenticationResponseOpts = { response: body, expectedChallenge: currentChallenge, expectedOrigin: origin, expectedRPID: rpID, authenticator: dbCredential, }; verification = await verifyAuthenticationResponse(opts); const { verified, authenticationInfo } = verification; if (verified) { await credentialService.updateCredentialCounter( uint8ArrayToBase64(bodyCredIDBuffer), authenticationInfo.newCounter, ); res.send({ verified: true }); } else { next(new CustomError("Verification failed", 400)); } } catch (error) { next( error instanceof CustomError ? error : new CustomError("Internal Server Error", 500), ); } finally { req.session.currentChallenge = undefined; req.session.loggedInUserId = undefined; } };
Quy trình xác thực (đăng nhập) của chúng tôi bao gồm hai điểm cuối:
1. Điểm cuối handleLoginStart:
Điểm cuối này được kích hoạt khi người dùng cố gắng đăng nhập. Đầu tiên, nó kiểm tra xem tên người dùng có tồn tại trong cơ sở dữ liệu hay không, trả về lỗi nếu không tìm thấy. Trong một kịch bản thực tế, bạn có thể đề nghị tạo một tài khoản mới thay thế.
Đối với người dùng hiện tại, nó truy xuất ID người dùng từ cơ sở dữ liệu, lưu trữ nó trong
phiên và tạo các tùy chọn
PublicKeyCredentialRequestOptions.
allowCredentials
được để trống để tránh hạn chế việc sử dụng thông tin xác thực. Đó là
lý do tại sao tất cả các passkey có sẵn cho relying party này có thể được chọn trong cửa
sổ passkey.
Challenge được tạo ra cũng được lưu trữ trong phiên và PublicKeyCredentialRequestOptions được gửi trở lại frontend.
PublicKeyCredentialRequestOptions bao gồm các dữ liệu sau:
dictionary PublicKeyCredentialRequestOptions { required BufferSource challenge; unsigned long timeout; USVString rpId; sequence<PublicKeyCredentialDescriptor> allowCredentials = []; DOMString userVerification = "preferred"; AuthenticationExtensionsClientInputs extensions; };
2. Điểm cuối handleLoginFinish:
Điểm cuối này truy xuất currentChallenge
và loggedInUserId
từ phiên.
Nó truy vấn cơ sở dữ liệu để tìm thông tin xác thực phù hợp bằng cách sử dụng
credential ID từ body. Nếu thông tin xác thực được
tìm thấy, điều này có nghĩa là người dùng được liên kết với
credential ID này bây giờ có thể được xác thực (đăng
nhập). Sau đó, chúng ta có thể truy vấn người dùng từ bảng người dùng thông qua ID người
dùng mà chúng ta nhận được từ thông tin xác thực và xác minh authenticationResponse
bằng
cách sử dụng challenge và body của yêu cầu. Nếu mọi thứ thành công, chúng ta hiển thị
thông báo đăng nhập thành công. Nếu không tìm thấy thông tin xác thực phù hợp, một lỗi sẽ
được gửi đi.
Ngoài ra, nếu xác minh thành công, bộ đếm của thông tin xác thực sẽ được cập nhật, challenge đã sử dụng và loggedInUserId sẽ bị xóa khỏi phiên.
Trên hết, chúng ta có thể xóa thư mục src/app
và src/constant
cùng với tất cả các tệp
trong đó.
Lưu ý: Quản lý phiên và bảo vệ route đúng cách, rất quan trọng trong các ứng dụng thực tế, được bỏ qua ở đây để đơn giản hóa trong hướng dẫn này.
Cuối cùng nhưng không kém phần quan trọng, chúng ta cần đảm bảo rằng các controller của
chúng ta có thể truy cập được bằng cách thêm các route thích hợp vào routes.ts
trong một
thư mục mới src/routes
:
routes.tsimport express from "express"; import { handleError } from "../middleware/errorHandler"; import { handleRegisterStart, handleRegisterFinish } from "../controllers/registration"; import { handleLoginStart, handleLoginFinish } from "../controllers/authentication"; const router = express.Router(); router.post("/registerStart", handleRegisterStart); router.post("/registerFinish", handleRegisterFinish); router.post("/loginStart", handleLoginStart); router.post("/loginFinish", handleLoginFinish); router.use(handleError); export { router };
Phần này của hướng dẫn passkey tập trung vào cách hỗ trợ passkey trong frontend của ứng
dụng của bạn. Chúng ta có một frontend rất cơ bản bao gồm ba tệp: index.html
,
styles.css
và script.js
. Cả ba tệp đều nằm trong một thư mục src/public
mới.
Tệp index.html
chứa một trường nhập cho tên người dùng và hai nút để đăng ký và đăng
nhập. Hơn nữa, chúng tôi nhập script @simplewebauthn/browser
giúp đơn giản hóa việc
tương tác với API Xác thực Web của trình duyệt trong tệp js/script.js
.
index.html
trông như thế này:
index.html<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Passkey Tutorial</title> <link rel="stylesheet" href="css/style.css" /> </head> <body> <div class="container"> <h1>Passkey Tutorial</h1> <div id="message"></div> <div class="input-group"> <input type="text" id="username" placeholder="Enter username" /> <button id="registerButton">Register</button> <button id="loginButton">Login</button> </div> </div> <script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.es5.umd.min.js"></script> <script src="js/script.js"></script> </body> </html>
script.js
trông như sau:
script.jsdocument.getElementById("registerButton").addEventListener("click", register); document.getElementById("loginButton").addEventListener("click", login); function showMessage(message, isError = false) { const messageElement = document.getElementById("message"); messageElement.textContent = message; messageElement.style.color = isError ? "red" : "green"; } async function register() { // Retrieve the username from the input field const username = document.getElementById("username").value; try { // Get registration options from your server. Here, we also receive the challenge. const response = await fetch("/api/passkey/registerStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); console.log(response); // Check if the registration options are ok. if (!response.ok) { throw new Error( "User already exists or failed to get registration options from server", ); } // Convert the registration options to JSON. const options = await response.json(); console.log(options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new attestation is created. This also means a new public-private-key pair is created. const attestationResponse = await SimpleWebAuthnBrowser.startRegistration(options); // Send attestationResponse back to server for verification and storage. const verificationResponse = await fetch("/api/passkey/registerFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(attestationResponse), }); if (verificationResponse.ok) { showMessage("Registration successful"); } else { showMessage("Registration failed", true); } } catch (error) { showMessage("Error: " + error.message, true); } } async function login() { // Retrieve the username from the input field const username = document.getElementById("username").value; try { // Get login options from your server. Here, we also receive the challenge. const response = await fetch("/api/passkey/loginStart", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: username }), }); // Check if the login options are ok. if (!response.ok) { throw new Error("Failed to get login options from server"); } // Convert the login options to JSON. const options = await response.json(); console.log(options); // This triggers the browser to display the passkey / WebAuthn modal (e.g. Face ID, Touch ID, Windows Hello). // A new assertionResponse is created. This also means that the challenge has been signed. const assertionResponse = await SimpleWebAuthnBrowser.startAuthentication(options); // Send assertionResponse back to server for verification. const verificationResponse = await fetch("/api/passkey/loginFinish", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(assertionResponse), }); if (verificationResponse.ok) { showMessage("Login successful"); } else { showMessage("Login failed", true); } } catch (error) { showMessage("Error: " + error.message, true); } }
Trong script.js
, có ba chức năng chính:
1. Hàm showMessage:
Đây là một hàm tiện ích được sử dụng chủ yếu để hiển thị thông báo lỗi, hỗ trợ việc gỡ lỗi.
2. Hàm Register:
Được kích hoạt khi người dùng nhấp vào "Register". Nó trích xuất tên người dùng từ trường
nhập và gửi nó đến điểm cuối passkeyRegisterStart. Phản hồi bao gồm
PublicKeyCredentialCreationOptions, được chuyển đổi thành JSON và chuyển cho
SimpleWebAuthnBrowser.startRegistration
. Lệnh gọi này kích hoạt authenticator của thiết
bị (như Face ID hoặc Touch ID). Sau khi xác thực cục bộ thành công, challenge đã được ký
sẽ được gửi trở lại điểm cuối passkeyRegisterFinish
, hoàn tất quá trình tạo passkey.
Trong quá trình đăng ký (sign-up), đối tượng attestation đóng một vai trò quan trọng, vì vậy hãy xem xét kỹ hơn về nó.
Đối tượng attestation chủ yếu bao gồm ba thành phần: fmt
,
attStmt
, và authData
. Phần tử fmt
biểu thị định dạng của câu lệnh
attestation, trong khi attStmt
đại diện cho chính câu lệnh
attestation. Trong các trường hợp mà attestation được coi là không cần thiết, fmt
sẽ
được chỉ định là "none", dẫn đến một attStmt
trống.
Trọng tâm là phân đoạn authData
trong cấu trúc này. Phân đoạn này là chìa khóa để truy
xuất các yếu tố cần thiết như ID relying party, các cờ, bộ đếm và dữ liệu thông tin xác
thực đã được chứng thực trên máy chủ của chúng ta. Về các cờ, điều đặc biệt quan tâm là BS
(Backup State) và BE (Backup Eligibility) cung cấp thêm thông tin nếu một passkey được
đồng bộ hóa (ví dụ: qua iCloud Keychain hoặc
1Password). Bên cạnh đó, UV (User
Verification) và UP (User Presence) cung cấp thêm thông tin hữu ích.
Điều quan trọng cần lưu ý là các phần khác nhau của đối tượng attestation, bao gồm dữ liệu authenticator, ID relying party, và câu lệnh attestation, đều được băm hoặc ký điện tử bởi authenticator bằng khóa riêng của nó. Quá trình này là không thể thiếu để duy trì tính toàn vẹn tổng thể của đối tượng attestation.
3. Hàm Login:
Được kích hoạt khi người dùng nhấp vào "Login". Tương tự như hàm đăng ký, nó trích xuất
tên người dùng và gửi nó đến điểm cuối passkeyLoginStart
. Phản hồi, chứa
PublicKeyCredentialRequestOptions, được chuyển đổi thành JSON và được sử dụng với
SimpleWebAuthnBrowser.startAuthentication
. Điều này kích hoạt xác thực cục bộ trên thiết
bị. Challenge đã được ký sau đó được gửi trở lại điểm cuối passkeyLoginFinish
. Một phản
hồi thành công từ điểm cuối này cho biết người dùng đã đăng nhập thành công vào ứng dụng.
Ngoài ra, tệp CSS đi kèm cung cấp kiểu dáng đơn giản cho ứng dụng:
body { font-family: "Helvetica Neue", Arial, sans-serif; text-align: center; padding: 40px; background-color: #f3f4f6; color: #333; } .container { max-width: 400px; margin: auto; background: white; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border-radius: 8px; } h1 { color: #007bff; font-size: 24px; margin-bottom: 20px; } .input-group { margin-bottom: 20px; } input[type="text"] { padding: 10px; margin-bottom: 10px; border: 1px solid #ced4da; border-radius: 4px; width: calc(100% - 22px); } button { width: calc(50% - 20px); padding: 10px 0; margin: 5px; font-size: 16px; cursor: pointer; border: none; border-radius: 4px; background-color: #007bff; color: white; } button:hover { background-color: #0056b3; } #message { color: #dc3545; margin: 20px; }
Để xem ứng dụng của bạn hoạt động, hãy biên dịch và chạy mã TypeScript của bạn với:
npm run dev
Máy chủ của bạn bây giờ sẽ hoạt động tại http://localhost:8080.
Những lưu ý cho môi trường Production:
Hãy nhớ rằng, những gì chúng ta đã đề cập là một phác thảo cơ bản. Khi triển khai một ứng dụng passkey trong môi trường sản phẩm, bạn cần đi sâu hơn vào:
Chúng ta đã thiết lập một container Docker cho cơ sở dữ liệu của mình. Tiếp theo, chúng ta
sẽ mở rộng thiết lập Docker Compose để bao gồm cả máy chủ với cả backend và frontend. Tệp
docker-compose.yml
của bạn nên được cập nhật tương ứng.
Để container hóa ứng dụng của chúng ta, chúng ta tạo một Dockerfile mới để cài đặt các gói cần thiết và khởi động máy chủ phát triển:
Docker# Use an official Node runtime as a parent image FROM node:20-alpine # Set the working directory in the container WORKDIR /usr/src/app # Copy package.json and package-lock.json COPY package*.json ./ # Install any needed packages RUN npm install # Bundle your app's source code inside the Docker image COPY . . # Make port 8080 available to the world outside this container EXPOSE 8080 # Define the command to run your app CMD ["npm", "run", "dev"]
Sau đó, chúng ta cũng mở rộng tệp docker-compose.yml
để khởi động container này:
docker-compose.ymlversion: "3.1" services: db: image: mysql command: --default-authentication-plugin=mysql_native_password restart: always environment: MYSQL_ROOT_PASSWORD: my-secret-pw MYSQL_DATABASE: webauthn_db ports: - "3306:3306" volumes: - ./init-db.sql:/docker-entrypoint-initdb.d/init-db.sql app: build: . ports: - "8080:8080" environment: - DB_HOST=db - DB_USER=root - DB_PASSWORD=my-secret-pw - DB_NAME=webauthn_db - SESSION_SECRET=secret123 depends_on: - db
Nếu bây giờ bạn chạy docker compose up
trong terminal của mình và truy cập
http://localhost:8080, bạn sẽ thấy phiên bản hoạt động của ứng
dụng web passkey của mình (ở đây đang chạy trên Windows 11
23H2 + Chrome 119):
Vì chúng tôi đã làm việc khá lâu với việc triển khai passkey, chúng tôi đã gặp phải một vài thách thức khi bạn làm việc trên các ứng dụng passkey thực tế:
Hơn nữa, chúng tôi có những mẹo sau đây cho các nhà phát triển khi nói đến phần triển khai:
Sử dụng Passkeys Debugger
Passkeys debugger giúp kiểm tra các cài đặt máy chủ WebAuthn và phản hồi của client khác nhau. Hơn nữa, nó cung cấp một trình phân tích tuyệt vời cho các phản hồi của authenticator.
Gỡ lỗi với tính năng Device Log của Chrome
Sử dụng nhật ký thiết bị của Chrome (có thể truy cập qua chrome://device-log/) để theo dõi các lệnh gọi FIDO/WebAuthn. Tính năng này cung cấp nhật ký thời gian thực của quá trình xác thực (đăng nhập), cho phép bạn xem dữ liệu đang được trao đổi và khắc phục mọi sự cố phát sinh.
Một phím tắt rất hữu ích khác để xem tất cả các passkey của bạn trong Chrome là sử dụng chrome://settings/passkeys.
Sử dụng Chrome Virtual WebAuthn Authenticator
Để tránh sử dụng lời nhắc Touch ID, Face ID hoặc Windows Hello trong quá trình phát triển, Chrome đi kèm với một authenticator WebAuthn ảo rất tiện dụng mô phỏng một authenticator thực. Chúng tôi thực sự khuyên bạn nên sử dụng nó để tăng tốc mọi thứ. Tìm thêm chi tiết tại đây.
Kiểm tra trên các nền tảng và trình duyệt khác nhau
Đảm bảo khả năng tương thích và chức năng trên các trình duyệt và nền tảng khác nhau. WebAuthn hoạt động khác nhau trên các trình duyệt khác nhau, vì vậy việc kiểm tra kỹ lưỡng là chìa khóa.
Kiểm tra trên các thiết bị khác nhau
Ở đây, việc làm việc với các công cụ như ngrok là đặc biệt hữu ích, nơi bạn có thể làm cho ứng dụng cục bộ của mình có thể truy cập được trên các thiết bị (di động) khác.
Đặt User Verification thành Preferred
Khi xác định các thuộc tính cho userVerification trong
PublicKeyCredentialRequestOptions, hãy chọn đặt chúng thành preferred
vì đây là một
sự cân bằng tốt giữa khả năng sử dụng và bảo mật. Điều này có nghĩa là các kiểm tra bảo
mật được áp dụng trên các thiết bị phù hợp trong khi vẫn giữ được sự thân thiện với người
dùng trên các thiết bị không có khả năng sinh trắc học.
Chúng tôi hy vọng hướng dẫn passkey này cung cấp một sự hiểu biết rõ ràng về cách triển khai passkey một cách hiệu quả. Trong suốt hướng dẫn, chúng tôi đã đi qua các bước cần thiết để tạo một ứng dụng passkey, tập trung vào các khái niệm cơ bản và triển khai thực tế. Mặc dù hướng dẫn này đóng vai trò là điểm khởi đầu, nhưng còn rất nhiều điều để khám phá và hoàn thiện trong thế giới WebAuthn.
Chúng tôi khuyến khích các nhà phát triển đi sâu hơn vào các sắc thái của passkey (ví dụ: thêm nhiều passkey, kiểm tra tính sẵn sàng của passkey trên các thiết bị hoặc cung cấp các giải pháp khôi phục). Đó là một hành trình đáng để bắt đầu, mang lại cả thách thức và phần thưởng to lớn trong việc tăng cường xác thực người dùng. Với passkey, bạn không chỉ xây dựng một tính năng; bạn đang góp phần vào một thế giới kỹ thuật số an toàn và thân thiện hơn với người dùng.
Enjoyed this read?
🤝 Join our Passkeys Community
Share passkeys implementation tips and get support to free the world from passwords.
🚀 Subscribe to Substack
Get the latest news, strategies, and insights about passkeys sent straight to your inbox.
Related Articles
Table of Contents