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#生物识别