WebAuthn 與 Passkey:告別密碼時代的無密碼認證完整實戰
前端工程(更新於 2026年6月2日)
密碼已死,Passkey 當立
密碼的根本問題:密碼是共享秘密——伺服器和使用者都知道,任何一方洩露都不安全。
| 問題 | 影響 |
|---|---|
| 弱密碼 | 81% 的資料洩露源於弱密碼 |
| 密碼複用 | 一個洩露,全部淪陷 |
| 釣魚攻擊 | 使用者無法區分真假網站 |
| 資料庫洩露 | 伺服器儲存的雜湊可能被破解 |
| MFA 簡訊劫持 | SIM swap 攻擊 |
Passkey(WebAuthn/FIDO2) 用公鑰加密替代密碼:
密碼模式:使用者 → [密碼] → 伺服器(儲存密碼雜湊)
Passkey模式:使用者 → [簽名] → 伺服器(僅存公鑰)
公鑰無法反推私鑰,伺服器被駭也無法冒充使用者。
WebAuthn 協議流程
註冊流程
1. 使用者點選"註冊 Passkey"
2. 伺服器生成 challenge → 傳送到前端
3. 瀏覽器呼叫 authenticator → 使用者驗證(指紋/Face ID/PIN)
4. Authenticator 生成金鑰對 → 返回公鑰 + 簽名
5. 前端傳送公鑰 + 簽名到伺服器
6. 伺服器驗證簽名 → 儲存公鑰(credential)
認證流程
1. 使用者點選"用 Passkey 登入"
2. 伺服器生成 challenge → 傳送到前端
3. 瀏覽器呼叫 authenticator → 使用者驗證
4. Authenticator 用私鑰簽名 challenge → 返回簽名
5. 前端傳送簽名到伺服器
6. 伺服器用公鑰驗證簽名 → 認證成功
完整程式碼實作
伺服器端:註冊開始
// POST /api/webauthn/register/start
import { generateRegistrationOptions } from '@simplewebauthn/server';
export async function registerStart(userId: string, username: string) {
const options = await generateRegistrationOptions({
rpID: 'example.com',
rpName: '工具庫',
userID: userId,
userName: username,
attestationType: 'none',
authenticatorSelection: {
authenticatorAttachment: 'platform', // 優先平台認證器
residentKey: 'required', // 可發現憑證(Passkey)
userVerification: 'preferred',
},
});
// 儲存 challenge 供後續驗證
await storeChallenge(userId, options.challenge);
return options;
}
前端:註冊
import { startRegistration } from '@simplewebauthn/browser';
async function registerPasskey() {
try {
// 1. 從伺服器獲取註冊選項
const options = await fetch('/api/webauthn/register/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id, username: user.name }),
}).then(r => r.json());
// 2. 呼叫瀏覽器 API 註冊
const credential = await startRegistration({ optionsJSON: options });
// 3. 傳送憑證到伺服器驗證
const result = await fetch('/api/webauthn/register/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id, credential }),
}).then(r => r.json());
if (result.verified) {
showToast('Passkey 註冊成功!');
}
} catch (error) {
if (error.name === 'NotAllowedError') {
showToast('使用者取消了操作');
}
}
}
伺服器端:註冊完成
import { verifyRegistrationResponse } from '@simplewebauthn/server';
export async function registerFinish(userId: string, credential: any) {
const challenge = await getChallenge(userId);
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge: challenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com',
});
if (verification.verified) {
// 儲存憑證(公鑰)
await storeCredential(userId, {
id: verification.registrationInfo?.credentialID,
publicKey: verification.registrationInfo?.credentialPublicKey,
counter: verification.registrationInfo?.counter,
deviceType: verification.registrationInfo?.credentialDeviceType,
});
}
return { verified: verification.verified };
}
前端:認證
import { startAuthentication } from '@simplewebauthn/browser';
async function authenticateWithPasskey() {
try {
// 1. 獲取認證選項
const options = await fetch('/api/webauthn/auth/start', {
method: 'POST',
}).then(r => r.json());
// 2. 呼叫瀏覽器 API 認證
const credential = await startAuthentication({ optionsJSON: options });
// 3. 驗證簽名
const result = await fetch('/api/webauthn/auth/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential }),
}).then(r => r.json());
if (result.verified) {
redirect('/dashboard');
}
} catch (error) {
if (error.name === 'NotAllowedError') {
showToast('認證被取消');
}
}
}
平台認證器 vs 漫遊認證器
| 維度 | 平台認證器 | 漫遊認證器 |
|---|---|---|
| 範例 | Touch ID, Face ID, Windows Hello | YubiKey, Titan Key |
| 儲存 | 裝置內 TPM/Secure Enclave | 硬體安全晶片 |
| 跨裝置 | iCloud/Google 同步 | 需物理攜帶 |
| 成本 | 免費 | $25-70 |
| 使用者體驗 | 指紋/面容,無感 | 插入/觸碰 |
| 遺失風險 | 裝置遺失 | 金鑰遺失 |
Passkey 同步機制
Apple iCloud Keychain:
iPhone 註冊 Passkey → iCloud 同步 → Mac/iPad 自動可用
Google Password Manager:
Android 註冊 Passkey → Google 同步 → Chrome 全平台可用
Windows Hello:
本地 TPM 儲存 → 不跨裝置同步
→ 建議同時註冊平台 + 漫遊 Passkey
降級策略:漸進式部署
function AuthForm() {
const [supportsPasskey, setSupportsPasskey] = useState(false);
useEffect(() => {
// 檢測 WebAuthn 支援
setSupportsPasskey(
window.PublicKeyCredential !== undefined &&
typeof window.PublicKeyCredential === 'function'
);
}, []);
return (
<form>
{supportsPasskey && (
<Button onClick={authenticateWithPasskey}>
用 Passkey 登入
</Button>
)}
{/* 傳統密碼作為降級 */}
<Input type="email" placeholder="信箱" />
<Input type="password" placeholder="密碼" />
<Button type="submit">登入</Button>
</form>
);
}
條件 UI:自動檢測 Passkey
// 檢測使用者是否有已註冊的 Passkey
const isConditionalAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
if (isConditionalAvailable) {
// 瀏覽器會在密碼框自動顯示 Passkey 選項
const credential = await startAuthentication({
optionsJSON: options,
useAutocomplete: true, // 啟用條件 UI
});
}
<!-- 密碼框會自動顯示 Passkey 選項 -->
<input
type="password"
autocomplete="webauthn"
placeholder="密碼或 Passkey"
/>
安全考量
1. Challenge 的安全性
// ✅ Challenge 必須是伺服器生成的隨機數
const challenge = crypto.randomUUID();
// ❌ 絕不要用固定值或時間戳
const badChallenge = Date.now().toString();
2. Origin 驗證
// ✅ 嚴格驗證 Origin,防止釣魚
const expectedOrigin = 'https://example.com';
// ❌ 不要用萬用字元
const badOrigin = '*';
3. 憑證計數器
// 檢測複製憑證
if (newCounter <= storedCounter) {
// 憑證可能被複製!
throw new Error('Possible cloned authenticator');
}
4. 多憑證策略
推薦:每個使用者至少註冊 2 個 Passkey
- 1 個平台 Passkey(日常使用)
- 1 個漫遊 Passkey 或備用 Passkey(恢復用)
瀏覽器與平台支援
| 平台 | Passkey 支援 | 同步 | 條件 UI |
|---|---|---|---|
| Chrome (Windows/macOS) | ✅ | ✅ | |
| Safari (macOS/iOS) | ✅ | iCloud | ✅ |
| Firefox | ✅ | 無 | ❌ |
| Edge | ✅ | ✅ | |
| Android Chrome | ✅ | ✅ | |
| iOS Safari | ✅ | iCloud | ✅ |
總結
Passkey 是密碼的終結者——基於公鑰加密,抗釣魚、抗洩露、抗重放。2026 年所有主流瀏覽器和作業系統均已支援,是時候在生產環境部署了。使用 @simplewebauthn 庫可以快速整合,配合條件 UI 讓使用者無感切換。記住降級策略:Passkey 優先,密碼兜底,漸進增強。
本站提供瀏覽器本地工具,免註冊即可試用 →
#WebAuthn#Passkey#无密码认证#FIDO2#生物识别