密碼作為預設驗證方式已數十年,仍帶來安全與可用性問題:使用者忘記、跨服務重用、不安全儲存。對開發者而言,密碼系統意味重設工單、釣魚風險與憑證外洩。
通行密鑰驗證是現代替代方案。本指南說明通行密鑰如何運作、WebAuthn API 實務長相,以及從註冊到通行密鑰登入的完整實作要點。
理解通行密鑰
通行密鑰是無密碼憑證,讓使用者以受信任裝置——智慧型手機、筆電或硬體安全金鑰——取代密碼登入。底層為公開金鑰密碼學:
- 公開金鑰存在應用程式伺服器。
- 私密金鑰安全存在使用者裝置,**永不離開**。
驗證時伺服器對裝置發出隨機挑戰;裝置以私密金鑰簽署後回傳;伺服器以公開金鑰驗證簽章。因私密金鑰不離開裝置,攻擊者無法透過釣魚或伺服器外洩竊取。
通行密鑰建構在 FIDO2 標準上,包含兩部分:
- WebAuthn(Web Authentication API)——開發者直接使用的瀏覽器端 API
- CTAP(Client to Authenticator Protocol)——瀏覽器與外接驗證器(如硬體安全金鑰)之間的通訊
開發者為何採用通行密鑰
Apple、Google、Microsoft 皆已內建通行密鑰支援,採用加速中,原因很實際:
- 釣魚抗性。 通行密鑰與註冊網域綁定。在
yourapp.com註冊的憑證不會在假網域yourapp-login.com生效。 - 伺服器無共享密鑰可外洩。 僅存公開金鑰;資料庫外洩也無法冒充使用者。
- 較佳 UX。 以 Face ID、Touch ID 或裝置 PIN 驗證——無密碼可忘或重設。
- 降低支援成本。 重設密碼是常見工單來源之一;通行密鑰從根本消除。
前置需求
HTTPS 與瀏覽器支援
WebAuthn 僅在 HTTPS 下運作。請確保 App 以有效 TLS 憑證提供服務——無例外,連 staging 亦然(本地開發可用 localhost,瀏覽器會豁免)。
Chrome、Safari、Firefox、Edge 皆支援 WebAuthn。覆蓋率可查 caniuse.com。
後端基礎建設
伺服器需處理三件事:
- 產生挑戰——每次註冊或登入使用隨機、單次使用的挑戰
- 驗證回應——驗證驗證器回傳的簽署斷言(assertion)
- 憑證儲存——每使用者儲存公開金鑰、credential ID、簽章計數器等
不要自行實作密碼學驗證。 請使用維護中的 WebAuthn 伺服器函式庫,例如 Go 的 go-webauthn、Python 的 py_webauthn、Node 的 @passwordless-id/webauthn。
帳戶復原
上線前就要規劃復原。若使用者僅註冊一臺裝置又遺失,需要回復管道。常見作法:允許多裝置註冊、改以電子郵件驗證、或備援碼。沒有復原路徑會變成支援案件或流失客戶。
通行密鑰驗證如何運作
分兩階段:註冊(建立憑證)與通行密鑰登入(使用憑證)。
註冊流程
註冊建立金鑰對,並將公開金鑰連結到使用者帳戶。
- 使用者在 App 中觸發通行密鑰設定。
- 伺服器產生隨機挑戰並送到瀏覽器。
- 瀏覽器以挑戰與 relying party 資訊呼叫
navigator.credentials.create()。 - 作業系統提示驗證——Touch ID、Face ID、Windows Hello 或 PIN。
- 裝置產生金鑰對;私密金鑰存於安全隔離區;公開金鑰與 credential ID 回傳伺服器。
- 伺服器儲存公開金鑰與 credential ID,與帳戶關聯。
登入流程
- 使用者選擇「以通行密鑰登入」。
- 伺服器產生新的隨機挑戰。
- 瀏覽器以挑戰呼叫
navigator.credentials.get()。 - 作業系統找出相符憑證並提示生物辨識/PIN。
- 裝置以儲存的私密金鑰簽署挑戰。
- 伺服器以儲存公開金鑰驗證簽章;成功即登入。
WebAuthn 範例:註冊與登入
實務上由兩個瀏覽器方法處理。
navigator.credentials.create() — 通行密鑰註冊
// 先向伺服器取得註冊挑戰
const response = await fetch('/auth/passkey/register/begin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: currentUser.id }),
});
const options = await response.json();
// 伺服器回傳 PublicKeyCredentialCreationOptions。
// 瀏覽器需要將 challenge 從 base64url 解碼為二進位。
options.challenge = base64urlToBuffer(options.challenge);
options.user.id = base64urlToBuffer(options.user.id);
// 觸發驗證器
const credential = await navigator.credentials.create({ publicKey: options });
// 將新憑證送回伺服器儲存
await fetch('/auth/passkey/register/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: credential.id,
rawId: bufferToBase64url(credential.rawId),
response: {
attestationObject: bufferToBase64url(credential.response.attestationObject),
clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
},
type: credential.type,
}),
});
navigator.credentials.get() — 通行密鑰登入
// 向伺服器取得驗證挑戰
const response = await fetch('/auth/passkey/login/begin', { method: 'POST' });
const options = await response.json();
options.challenge = base64urlToBuffer(options.challenge);
// 若有 allowCredentials,每個 credential id 也要解碼
if (options.allowCredentials) {
options.allowCredentials = options.allowCredentials.map(c => ({
...c,
id: base64urlToBuffer(c.id),
}));
}
// 提示使用者——瀏覽器負責生物辨識/PIN UI
const assertion = await navigator.credentials.get({ publicKey: options });
// 將簽署後的 assertion 送回伺服器驗證
const verifyResponse = await fetch('/auth/passkey/login/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
id: assertion.id,
rawId: bufferToBase64url(assertion.rawId),
response: {
authenticatorData: bufferToBase64url(assertion.response.authenticatorData),
clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON),
signature: bufferToBase64url(assertion.response.signature),
userHandle: assertion.response.userHandle
? bufferToBase64url(assertion.response.userHandle)
: null,
},
type: assertion.type,
}),
});
if (verifyResponse.ok) {
// 驗證成功——重新導向或更新 UI
}
base64urlToBuffer 與 bufferToBase64url 在伺服器傳來的 base64url 字串與 WebAuthn 需要的 ArrayBuffer 之間轉換。可自行實作或使用處理編碼的函式庫。
⚠️ 常見錯誤: 將挑戰以字串而非
ArrayBuffer傳入會導致navigator.credentials.create()拋錯。務必在交給 WebAuthn API 前將伺服器回傳的 base64url 解碼。
與驗證平台整合
從零建置 WebAuthn 可行,但涵蓋面大——挑戰管理、認證(attestation)驗證、簽章計數器、多裝置同步等。許多團隊改採驗證平台代管。
Authgear 內建通行密鑰註冊與登入。將 App 接上 Authgear 後,WebAuthn 儀式、憑證儲存與裝置管理由平台處理。若團隊核心產品不是驗證基礎建設,值得評估。了解 Authgear 的通行密鑰支援。
測試通行密鑰實作
上線前請跨環境測試:
- 註冊——至少在兩種不同裝置/瀏覽器建立通行密鑰
- 登入——端到端驗證完整流程
- 多裝置——一臺註冊、另一臺登入(經 iCloud 鑰匙圈或 Google 密碼管理員同步的通行密鑰)
- 硬體安全金鑰——若需支援 FIDO2 金鑰請實測
- 復原——模擬遺失裝置並走完整復原流程
Chrome DevTools 有 WebAuthn 模擬器(DevTools → More tools → WebAuthn),無實體驗證器亦可測註冊與驗證。
最佳實踐
註冊多臺裝置
引導使用者在入職/註冊時至少註冊兩臺裝置。僅一臺又遺失會被鎖在門外。多裝置或「同步通行密鑰 + 硬體金鑰」提供自然後援。
為不熟悉的使用者設計引導
許多人從未看過通行密鑰提示。在呼叫 navigator.credentials.create() 前簡短說明:通行密鑰是什麼、接下來會發生什麼、為何比密碼安全。困惑的使用者會按取消且不再嘗試。
每次產生新挑戰
每次註冊與登入都必須使用伺服器產生的唯一隨機挑戰(至少 16 bytes,建議 32)。挑戰須單次使用且短效(例如 5 分鐘內過期)。重用或接受過期挑戰會開啟重放攻擊大門。
驗證簽章計數器
驗證器維護每次使用遞增的簽章計數器。伺服器應儲存並比對;若收到計數器小於已儲存值,可能表示憑證被複製——應標記並要求重新驗證。
支援跨平台通行密鑰
通行密鑰可為裝置綁定(如硬體金鑰)或同步型(經 iCloud 鑰匙圈、Google 密碼管理員、1Password 等備份)。若也要允許硬體金鑰,不要一律設 authenticatorAttachment: "platform"。
記錄驗證事件
在伺服器端記錄註冊嘗試、成功登入、驗證失敗、憑證刪除等。這些紀錄對偵測異常(如同一憑證反覆驗證失敗)與使用者回報問題時除錯不可或缺。
結論
通行密鑰以留在使用者裝置上的密碼學金鑰對取代共享密鑰,因此具釣魚抗性、不受憑證資料庫外洩影響,且對使用者更快。
WebAuthn 在所有現代瀏覽器支援良好;客戶端程式相對直觀;複雜度主要在伺服器——挑戰管理、assertion 驗證與憑證儲存。請用伺服器函式庫處理密碼學重運算,勿自行實作。
若不想自行維運基礎建設,Authgear 提供開箱即用的通行密鑰註冊與登入,讓團隊專注在產品本身。
常見問題
什麼是通行密鑰驗證?
以公開金鑰密碼學取代密碼的無密碼登入。裝置持有私密金鑰;伺服器存公開金鑰。登入時伺服器發挑戰,裝置在生物辨識/PIN 後以私密金鑰簽署,伺服器驗證簽章。不建立、不儲存、不傳輸密碼。
什麼是通行密鑰?
通行密鑰是通行密鑰驗證所建立的憑證:伺服器上的公開金鑰 + 鎖在使用者裝置的私密金鑰。使用者以生物辨識或裝置 PIN 驗證;私密金鑰不離開裝置。
通行密鑰技術上如何運作?
公開金鑰密碼學。裝置持私密金鑰;伺服器存對應公開金鑰。登入時伺服器發挑戰,裝置以私密金鑰簽署,伺服器驗證簽章。無共享密鑰在線上傳輸。
什麼是 WebAuthn?
Web Authentication API,W3C 標準,讓應用程式建立與使用通行密鑰。所有主流瀏覽器皆支援。
通行密鑰比密碼安全嗎?
是。無法釣魚(網域綁定)、伺服器外洩僅公開金鑰、無法跨站重用,也無密碼可暴力猜測。
通行密鑰能跨多裝置同步嗎?
可以。Apple(iCloud 鑰匙圈)、Google(密碼管理員)、Microsoft(Windows Hello)可跨使用者裝置同步。1Password、Dashlane 等第三方密碼管理員亦支援。
有可在本機跑的 WebAuthn 範例嗎?
有。webauthn.io 為互動示範,無需設定即可在瀏覽器測試。Google 的 Build your first WebAuthn app codelab 可走完整註冊與驗證流程。本文程式片段即為你自有 App 中會使用的客戶端呼叫方式。