Upvotes
0
Upvotes: 0好, 請說明Passkeys 是如何運作, 如果可以也請提供範例程式碼, 最好在Windows, MacOS與Linux上面的都提供參考
Passkeys(通行金鑰)被譽為「密碼殺手」,它的出現是為了徹底解決人類「記不住長密碼」與「密碼容易被盜」這兩個宿命。
簡單來說,Passkeys 是基於 FIDO2 / WebAuthn 標準的非對稱加密技術。你不再需要輸入字串,而是改用設備上的生物辨識(指紋、臉部掃描)或 PIN 碼來解鎖儲存在硬體中的秘密。
Passkeys 不像密碼那樣是一個「共享的秘密」(你跟伺服器都知道同一個字串)。它使用的是公鑰 (Public Key) 與 私鑰 (Private Key) 的組合:
雖然前端程式碼是一樣的,但不同作業系統在底層處理私鑰的方式有所不同:
| 作業系統 | 底層硬體技術 | 使用者驗證方式 | 跨裝置同步 |
|---|---|---|---|
| Windows | TPM (信賴平台模組) | Windows Hello (臉部、指紋、PIN) | 透過 Microsoft 帳戶 (較受限) |
| macOS / iOS | Secure Enclave | Touch ID / Face ID | iCloud Keychain (無縫同步) |
| Linux | TPM / libfido2 | 實體安全金鑰 (如 YubiKey) | 視瀏覽器與硬體金鑰而定 |
| Android | TEE (可信執行環境) | 指紋、圖形鎖 | Google 密碼管理員 |
Passkeys 是透過瀏覽器的 navigator.credentials API 實現的。這段程式碼在 Windows、macOS、Linux 的現代瀏覽器中是通用的,因為瀏覽器會負責去叫用作業系統底層的驗證介面。
這是讓使用者建立新金鑰的過程。
async function registerPasskey() {
// 1. 從後端取得註冊參數 (通常包含挑戰值與使用者資訊)
const optionsFromServer = await fetch('/generate-registration-options');
const options = await optionsFromServer.json();
// 轉換格式 (Base64URL -> Uint8Array)
options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
options.user.id = Uint8Array.from(atob(options.user.id), c => c.charCodeAt(0));
try {
// 2. 呼叫瀏覽器 API,這時會彈出系統視窗 (Windows Hello / Touch ID)
const credential = await navigator.credentials.create({
publicKey: options
});
// 3. 將生成的公鑰與憑證傳回後端儲存
await fetch('/verify-registration', {
method: 'POST',
body: JSON.stringify(credential)
});
alert("註冊成功!");
} catch (err) {
console.error("註冊失敗", err);
}
}
這是當使用者下次回來時,證明自己身分的過程。
async function loginWithPasskey() {
// 1. 從後端取得登入挑戰
const optionsFromServer = await fetch('/generate-authentication-options');
const options = await optionsFromServer.json();
options.challenge = Uint8Array.from(atob(options.challenge), c => c.charCodeAt(0));
try {
// 2. 調用生物辨識驗證
const assertion = await navigator.credentials.get({
publicKey: options
});
// 3. 將簽章傳回後端驗證
const result = await fetch('/verify-authentication', {
method: 'POST',
body: JSON.stringify(assertion)
});
if (result.ok) {
window.location.href = "/dashboard";
}
} catch (err) {
console.error("登入失敗", err);
}
}
g00gle.com,你的瀏覽器會發現網址不對,根本不會跳出 Passkey 驗證視窗。Passkey 結合了 Local Storage 的存取速度 與 硬體層級的防護力量。它解決了 XSS 竊取的問題(因為 JS 拿不到私鑰),也解決了密碼外洩的問題(因為根本沒密碼)。
如果你想在自己的專案中實作 Passkeys,通常最難的部分在於後端的「簽章驗證邏輯」。你需要我介紹一些流行的後端函式庫(如 Node.js 或 Python)來簡化這個過程嗎?
好, 我需要Node.js及Python FastAPI兩種後端函示庫的範例, 請提供
實作 Passkeys(WebAuthn)最困難的部分在於後端的二進位資料處理與加密簽章驗證。幸運的是,Node.js 與 Python 都有成熟的函式庫能幫你處理這些複雜細節。
在實作之前,請記住後端的核心任務只有兩個:
@simplewebauthn/server這是目前 Node.js 生態系中最受歡迎、文件最齊全的函式庫。
npm install @simplewebauthn/server
const {
generateRegistrationOptions,
verifyRegistrationResponse,
} = require('@simplewebauthn/server');
// 1. 註冊第一步:產生註冊選項給前端
app.get('/generate-registration-options', async (req, res) => {
const options = await generateRegistrationOptions({
rpName: '我的安全網站',
rpID: 'localhost',
userID: 'user_123', // 應從資料庫獲取
userName: 'user@example.com',
attestationType: 'none',
authenticatorSelection: {
residentKey: 'required',
userVerification: 'preferred',
},
});
// 重要:挑戰值 (challenge) 必須存入 Session,稍後驗證時比對
req.session.currentChallenge = options.challenge;
res.json(options);
});
// 2. 註冊第二步:驗證前端傳回的公鑰
app.post('/verify-registration', async (req, res) => {
const { body } = req;
const expectedChallenge = req.session.currentChallenge;
const verification = await verifyRegistrationResponse({
response: body,
expectedChallenge,
expectedOrigin: 'http://localhost:3000',
expectedRPID: 'localhost',
});
if (verification.verified) {
// 將 verification.registrationInfo.credentialID 與 credentialPublicKey 存入資料庫
res.json({ success: true });
}
});
webauthnPython 社群通常使用由 Duo Labs 維護的 webauthn 函式庫,它非常嚴謹且符合標準。
pip install webauthn fastapi uvicorn
from fastapi import FastAPI, Request
from webauthn import (
generate_registration_options,
verify_registration_response,
options_to_json,
)
from webauthn.helpers.structs import RegistrationCredential
app = FastAPI()
# 模擬資料庫與 Session
DB_USER_ID = "user_123"
RP_ID = "localhost"
RP_NAME = "My Python App"
temporary_storage = {}
@app.get("/generate-registration-options")
async def registration_options():
options = generate_registration_options(
rp_id=RP_ID,
rp_name=RP_NAME,
user_id=DB_USER_ID,
user_name="user@example.com",
)
# 儲存挑戰值供後續驗證
temporary_storage["challenge"] = options.challenge
return options_to_json(options)
@app.post("/verify-registration")
async def verify_registration(credential: dict):
expected_challenge = temporary_storage.get("challenge")
try:
verification = verify_registration_response(
credential=credential,
expected_challenge=expected_challenge,
expected_origin="http://localhost:8000",
expected_rp_id=RP_ID,
)
# 驗證成功後,將 verification.credential_id 和 public_key 存入資料庫
return {"verified": True}
except Exception as e:
return {"verified": False, "error": str(e)}
example.com)。注意: 如果你在本地測試,必須使用 localhost。如果透過 IP 存取(如 192.168...),大多數瀏覽器會因為安全限制拒絕執行 WebAuthn。
Binary 轉換:
WebAuthn 原生處理的是 ArrayBuffer。當你在前端收到 credential 後,直接用 JSON.stringify 可能會出錯。建議使用像 simplewebauthn/browser 的前端庫來簡化轉換工作。
HTTPS 是強制性的:
除了 localhost 以外,Passkeys 必須在 HTTPS 環境下運作。
公鑰儲存:
public_key。請將它以二進位 (Blob/Buffer) 格式存入資料庫。登入時,你需要根據使用者的 credential_id 撈出對應的公鑰來驗證簽章。實作 Passkeys 後,你的系統將達到金融級的安全標準。
你會希望我提供一段前端的「萬用處理腳本」,讓你能輕鬆將剛才提到的二進位資料格式化並傳送給 Node.js 或 Python 後端嗎?
好, 請提供一段前端的「萬用處理腳本」
實作 Passkeys 最繁瑣的地方在於資料格式的轉換。WebAuthn API 規定所有金鑰相關資料都必須是 ArrayBuffer(二進位格式),但後端 API 通常只能傳輸 JSON 格式(Base64 字串)。
這段「萬用前端腳本」的核心任務就是處理 Base64URL $\leftrightarrow$ Uint8Array 的轉換,並提供註冊與登入的標準介面。
@simplewebauthn/browser在 2026 年,最穩定的做法是使用成熟的函式庫。它已經幫你寫好了所有的轉換邏輯。
安裝:
npm install @simplewebauthn/browser
萬用邏輯:
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
// 註冊 Passkey
async function handleRegister() {
const resp = await fetch('/generate-registration-options');
const optionsJSON = await resp.json();
let regResp;
try {
// 這個函式會處理所有二進位轉換,並跳出系統驗證視窗 (Windows Hello/TouchID)
regResp = await startRegistration({ optionsJSON });
} catch (error) {
console.error('註冊中斷:', error);
return;
}
await fetch('/verify-registration', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(regResp),
});
}
如果你不想安裝套件,這段腳本包含了解析 WebAuthn 必備的轉換工具函數,適用於所有現代瀏覽器。
/**
* 萬用 WebAuthn 處理器 (Vanilla JS)
*/
const PasskeyHelper = {
// 輔助工具:將 Base64URL 轉為 ArrayBuffer
bufferFromBase64(base64url) {
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
const binStr = window.atob(base64);
const bin = new Uint8Array(binStr.length);
for (let i = 0; i < binStr.length; i++) bin[i] = binStr.charCodeAt(i);
return bin.buffer;
},
// 輔助工具:將 ArrayBuffer 轉為 Base64URL (傳回後端用)
bufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
let str = '';
for (const b of bytes) str += String.fromCharCode(b);
return window.btoa(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
},
// 註冊流程
async create(options) {
// 轉換後端傳來的挑戰值與 ID
options.challenge = this.bufferFromBase64(options.challenge);
options.user.id = this.bufferFromBase64(options.user.id);
if (options.excludeCredentials) {
options.excludeCredentials.forEach(c => c.id = this.bufferFromBase64(c.id));
}
const credential = await navigator.credentials.create({ publicKey: options });
// 封裝結果傳回給後端
return {
id: credential.id,
rawId: this.bufferToBase64(credential.rawId),
type: credential.type,
response: {
attestationObject: this.bufferToBase64(credential.response.attestationObject),
clientDataJSON: this.bufferToBase64(credential.response.clientDataJSON),
}
};
},
// 登入流程
async get(options) {
options.challenge = this.bufferFromBase64(options.challenge);
if (options.allowCredentials) {
options.allowCredentials.forEach(c => c.id = this.bufferFromBase64(c.id));
}
const assertion = await navigator.credentials.get({ publicKey: options });
return {
id: assertion.id,
rawId: this.bufferToBase64(assertion.rawId),
type: assertion.type,
response: {
authenticatorData: this.bufferToBase64(assertion.response.authenticatorData),
clientDataJSON: this.bufferToBase64(assertion.response.clientDataJSON),
signature: this.bufferToBase64(assertion.response.signature),
userHandle: assertion.response.userHandle ? this.bufferToBase64(assertion.response.userHandle) : null,
}
};
}
};
這段腳本在所有平台運作邏輯一致,但瀏覽器調用的系統介面會根據 OS 改變:
| 平台 | 瀏覽器行為 | 系統反應 |
|---|---|---|
| Windows | 呼叫 Windows Hello API | 彈出藍色視窗要求輸入 PIN 碼、掃描臉部或指紋。 |
| macOS | 呼叫 Local Authentication | 螢幕右上角彈出視窗,要求按下 Touch ID 或輸入電腦密碼。 |
| Linux | 呼叫 libfido2 (視桌面環境) | 視瀏覽器版本,通常會提示插入 USB 硬體金鑰 (YubiKey) 或使用手機掃描 QR Code。 |
| Android/iOS | 呼叫系統通行金鑰管理員 | 彈出全螢幕底部的 Sheets,刷臉或指紋即可完成。 |
目前的趨勢是讓 Passkey 出現在密碼欄位的「自動填充」選單中,這不需要使用者點擊「Passkey 登入」按鈕。
實作方式:
<input> 加上 autocomplete="username webauthn"。navigator.credentials.get 時,傳入 mediation: 'conditional'。這能讓使用者就像使用傳統密碼管理器一樣,點一下欄位就完成生物辨識登入。
你想知道如何將這個「自動填充 (Autofill)」功能整合進剛才的腳本中,讓你的登入頁面更符合 2026 年的 UX 標準嗎?
Comments
Login to add a comment.
No comments yet.