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) Google
Safari (macOS/iOS) iCloud
Firefox
Edge Google
Android Chrome Google
iOS Safari iCloud

總結

Passkey 是密碼的終結者——基於公鑰加密,抗釣魚、抗洩露、抗重放。2026 年所有主流瀏覽器和作業系統均已支援,是時候在生產環境部署了。使用 @simplewebauthn 庫可以快速整合,配合條件 UI 讓使用者無感切換。記住降級策略:Passkey 優先,密碼兜底,漸進增強。

本站提供瀏覽器本地工具,免註冊即可試用 →

#WebAuthn#Passkey#无密码认证#FIDO2#生物识别