Passkeys 实战指南:WebAuthn 无密码认证的原理、实现与避坑

深度解析 Passkeys 与 WebAuthn 无密码认证技术,涵盖注册/认证完整流程、前后端代码实现、多设备同步策略、与传统 JWT 方案对比,附生产级避坑指南。

安全与密码 2026-05-29 18 分钟

2026 年,密码泄露事件仍在以每年 30% 的速度增长——RockYou2024 泄露了 100 亿条密码,Verizon 数据泄露报告指出 81% 的黑客攻击利用了弱密码或泄露密码。Passkeys(通行密钥)作为 FIDO2 联盟推动的无密码认证标准,已经被 Apple、Google、Microsoft 三大平台原生支持,覆盖全球超过 40 亿台设备。对于开发者而言,理解并实现 Passkeys 不再是「锦上添花」,而是认证系统的必选项。

本文不会停留在「Passkeys 是什么」的科普层面,而是直接深入 WebAuthn API 的技术细节,手把手实现完整的注册和认证流程,并与传统 JWT + 密码方案做详细对比,帮你判断何时切换、如何切换。

🔐 一、Passkeys 的技术原理与架构

1.1 公钥密码学在认证中的应用

Passkeys 的核心是非对称加密(Asymmetric Cryptography)。与传统密码方案不同,用户的认证凭证不是存储在服务器上的密码哈希,而是一对密钥:

  • 私钥(Private Key):存储在用户设备的安全芯片中(如 Apple Secure Enclave、Google Titan M),永远不会离开设备
  • 公钥(Public Key):注册时发送到服务器存储,用于验证签名

认证流程本质上是一个**挑战-响应(Challenge-Response)**过程:

  1. 服务器生成一个随机挑战(Challenge)发送给客户端
  2. 客户端用私钥对挑战进行签名
  3. 服务器用公钥验证签名

📌 **记住:**私钥永远不会传输到网络上,即使服务器被攻破,攻击者也无法获取用户的私钥——这从根本上消除了密码泄露的风险。

1.2 Passkeys vs 传统密码 vs WebAuthn 旧版

很多开发者容易混淆这几个概念,先理清它们的关系:

特性 传统密码 WebAuthn 安全密钥 Passkeys
用户记忆 需要记忆密码 不需要 不需要
私钥存储 N/A(无密钥对) 物理安全密钥 设备安全芯片 + 云同步
多设备同步 自动(密码共享) ❌ 不支持 ✅ 云同步
钓鱼攻击防护 ❌ 弱 ✅ 强(绑定 Origin) ✅ 强(绑定 Origin)
中间人攻击防护 ❌ 弱 ✅ 强 ✅ 强
用户体验 差(记忆负担) 中(需插拔设备) 优(生物识别)
设备丢失恢复 重置密码 需备用密钥 云同步自动恢复
实现复杂度

⚡ **关键结论:**Passkeys 本质上是「可云同步的 WebAuthn 凭证」——它继承了 WebAuthn 的安全性,同时解决了多设备同步的痛点。

1.3 平台认证器 vs 漫游认证器

WebAuthn 定义了两种认证器(Authenticator)类型:

平台认证器(Platform Authenticator)

  • 内置在设备中:Touch ID、Face ID、Windows Hello、Android 生物识别
  • 用户体验最好:指纹或面部识别即可完成认证
  • Passkeys 默认使用平台认证器

漫游认证器(Roaming Authenticator)

  • 外接设备:YubiKey、Google Titan Key
  • 通过 USB、NFC、BLE 连接
  • 适合高安全场景的二次认证

🚀 二、完整实现:从注册到认证

2.1 后端实现(Node.js + @simplewebauthn/server)

首先安装依赖:

npm install @simplewebauthn/server @simplewebauthn/browser

以下是一个完整的 Passkey 注册和认证后端实现。@simplewebauthn 是目前最成熟的 WebAuthn 库,封装了复杂的 CBOR 编解码和证书验证逻辑:

// server.js — Passkey 注册与认证完整后端
import {
  generateRegistrationOptions,
  verifyRegistrationResponse,
  generateAuthenticationOptions,
  verifyAuthenticationResponse,
} from '@simplewebauthn/server';
import express from 'express';

const app = express();
app.use(express.json());

// ⚠️ 生产环境必须替换为真实域名
const rpName = 'My App';
const rpID = 'localhost';
const expectedOrigin = `http://${rpID}:3000`;

// 模拟数据库存储(生产环境用 PostgreSQL/Redis)
const userDB = {};   // { userId: { id, username, passkeys: [] } }
const challengeDB = {}; // { userId: challenge }

// ========== 注册流程 ==========
// 第一步:生成注册选项
app.post('/api/register/start', async (req, res) => {
  const { username } = req.body;

  // 查找或创建用户
  let user = Object.values(userDB).find(u => u.username === username);
  if (!user) {
    const userId = `user_${Date.now()}`;
    user = { id: userId, username, passkeys: [] };
    userDB[userId] = user;
  }

  const options = await generateRegistrationOptions({
    rpName,
    rpID,
    userID: Buffer.from(user.id),
    userName: user.username,
    userDisplayName: user.username,
    // 排除已注册的凭证,防止重复注册
    excludeCredentials: user.passkeys.map(pk => ({
      id: pk.credentialID,
      type: 'public-key',
    })),
    authenticatorSelection: {
      // 要求平台认证器(Touch ID / Face ID)
      authenticatorAttachment: 'platform',
      // 允许无密码登录(Passkey 模式)
      residentKey: 'required',
      userVerification: 'required',
    },
  });

  // 临时存储 challenge,后续验证用
  challengeDB[user.id] = options.challenge;
  res.json(options);
});

// 第二步:验证注册响应
app.post('/api/register/verify', async (req, res) => {
  const { userId, credential } = req.body;
  const expectedChallenge = challengeDB[userId];

  try {
    const verification = await verifyRegistrationResponse({
      response: credential,
      expectedChallenge,
      expectedOrigin,
      expectedRPID: rpID,
    });

    if (verification.verified && verification.registrationInfo) {
      const { credentialPublicKey, credentialID, counter } =
        verification.registrationInfo;

      const user = userDB[userId];
      user.passkeys.push({
        credentialID: Buffer.from(credentialID).toString('base64url'),
        credentialPublicKey: Buffer.from(credentialPublicKey).toString('base64url'),
        counter,
      });
    }

    delete challengeDB[userId];
    res.json({ verified: verification.verified });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

// ========== 认证流程 ==========
// 第三步:生成认证选项
app.post('/api/login/start', async (req, res) => {
  const { username } = req.body;
  const user = Object.values(userDB).find(u => u.username === username);

  const options = await generateAuthenticationOptions({
    rpID,
    userVerification: 'required',
    // 允许所有已注册的凭证
    allowCredentials: user?.passkeys.map(pk => ({
      id: pk.credentialID,
      type: 'public-key',
    })) ?? [],
  });

  if (user) challengeDB[user.id] = options.challenge;
  res.json(options);
});

// 第四步:验证认证响应
app.post('/api/login/verify', async (req, res) => {
  const { username, credential } = req.body;
  const user = Object.values(userDB).find(u => u.username === username);
  if (!user) return res.status(404).json({ error: 'User not found' });

  const passkey = user.passkeys.find(
    pk => pk.credentialID === credential.id
  );
  if (!passkey) return res.status(404).json({ error: 'Passkey not found' });

  try {
    const verification = await verifyAuthenticationResponse({
      response: credential,
      expectedChallenge: challengeDB[user.id],
      expectedOrigin,
      expectedRPID: rpID,
      authenticator: {
        credentialPublicKey: Buffer.from(passkey.credentialPublicKey, 'base64url'),
        credentialID: Buffer.from(passkey.credentialID, 'base64url'),
        counter: passkey.counter,
      },
    });

    // 更新 counter 防重放攻击
    if (verification.verified) {
      passkey.counter = verification.authenticationInfo.newCounter;
    }

    delete challengeDB[user.id];
    res.json({ verified: verification.verified });
  } catch (error) {
    res.status(400).json({ error: error.message });
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));

2.2 前端实现(@simplewebauthn/browser)

前端代码使用 @simplewebauthn/browser 库,它封装了 navigator.credentials.create()navigator.credentials.get() 的复杂参数构造:

// auth.js — 前端 Passkey 注册与认证
import {
  startRegistration,
  startAuthentication,
  browserSupportsWebAuthn,
} from '@simplewebauthn/browser';

// 检查浏览器是否支持 WebAuthn
if (!browserSupportsWebAuthn()) {
  alert('您的浏览器不支持 Passkeys,请升级到最新版本');
}

// ========== 注册 Passkey ==========
async function registerPasskey(username) {
  try {
    // 第一步:从服务器获取注册选项
    const optionsResp = await fetch('/api/register/start', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username }),
    });
    const options = await optionsResp.json();

    // 第二步:调用浏览器 WebAuthn API 创建凭证
    // 这一步会弹出系统级的生物识别对话框
    const credential = await startRegistration({ optionsJSON: options });

    // 第三步:将凭证发送到服务器验证
    const verifyResp = await fetch('/api/register/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        userId: options.user.id,
        credential,
      }),
    });
    const result = await verifyResp.json();

    if (result.verified) {
      console.log('✅ Passkey 注册成功!');
    }
    return result;
  } catch (error) {
    // 常见错误:用户取消、设备不支持、超时
    if (error.name === 'NotAllowedError') {
      console.log('用户取消了注册');
    } else {
      console.error('注册失败:', error);
    }
    throw error;
  }
}

// ========== Passkey 认证 ==========
async function loginWithPasskey(username) {
  try {
    // 第一步:获取认证选项
    const optionsResp = await fetch('/api/login/start', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username }),
    });
    const options = await optionsResp.json();

    // 第二步:调用浏览器 WebAuthn API 进行认证
    // 系统弹出生物识别验证(Touch ID / Face ID / 指纹)
    const credential = await startAuthentication({ optionsJSON: options });

    // 第三步:发送到服务器验证签名
    const verifyResp = await fetch('/api/login/verify', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ username, credential }),
    });
    const result = await verifyResp.json();

    if (result.verified) {
      console.log('✅ 认证成功!');
    }
    return result;
  } catch (error) {
    console.error('认证失败:', error);
    throw error;
  }
}

// 导出供页面使用
window.registerPasskey = registerPasskey;
window.loginWithPasskey = loginWithPasskey;

⚠️ **警告:**WebAuthn API 只能在安全上下文(Secure Context)中使用——即 HTTPS 或 localhost。在 HTTP 环境下,navigator.credentials 会返回 undefined。开发环境务必使用 localhost 或配置自签名证书。

2.3 与现有系统的混合认证策略

生产环境中,不建议一刀切地完全替换密码。更务实的方案是渐进式迁移

// hybrid-auth.js — 混合认证策略
async function authenticate(username, method, payload) {
  switch (method) {
    case 'passkey':
      return loginWithPasskey(username);

    case 'password':
      // 传统密码登录
      const resp = await fetch('/api/login/password', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username, password: payload.password }),
      });
      return resp.json();

    case 'password+mfa':
      // 密码 + Passkey 作为二次验证
      const passwordResult = await authenticate(username, 'password', payload);
      if (!passwordResult.verified) return passwordResult;

      // 密码通过后,用 Passkey 做二次验证
      return loginWithPasskey(username);
  }
}

// 渐进式引导用户注册 Passkey
async function suggestPasskeyAfterLogin(userId) {
  const hasPasskey = await checkUserHasPasskey(userId);
  if (!hasPasskey) {
    showBanner({
      title: '启用 Passkey 无密码登录',
      description: '使用指纹或面部识别,下次登录更快更安全',
      action: () => registerPasskey(userId),
    });
  }
}

📊 三、方案对比与生产实战

3.1 Passkeys vs JWT + 密码:全维度对比

维度 JWT + 密码 Passkeys + WebAuthn
用户注册步骤 输入密码 + 确认密码 生物识别一次确认
平均登录耗时 15-30 秒(输入密码) 2-5 秒(指纹/面部)
密码重置流程 邮箱/短信验证 不需要(无私密密码)
服务端存储风险 密码哈希可能被破解 仅存储公钥,无风险
钓鱼防护 ❌ 无 ✅ Origin 绑定
中间人攻击防护 ❌ 无 ✅ 有
实现复杂度 ⭐ 低 ⭐⭐⭐ 中高
兼容性 100% 95%+(2026 年数据)
用户教育成本 有一定成本

⚡ **关键结论:**Passkeys 在安全性上碾压传统密码方案,但在实现复杂度和用户教育上需要额外投入。建议采用「密码 + Passkey 并存」的渐进式策略。

3.2 生产环境的常见坑与避坑指南

坑 1:Origin 验证不正确导致认证失败

WebAuthn 严格绑定 Origin(协议 + 域名 + 端口)。开发时用 localhost:3000,部署到 example.com 后所有已注册的 Passkey 都会失效。

💡 **提示:**开发环境和生产环境的 Origin 不同,Passkey 不可迁移。建议在开发阶段就使用与生产一致的域名(通过 hosts 文件映射),或做好用户引导重新注册的准备。

坑 2:CBOR 编解码格式问题

WebAuthn 使用 CBOR(Concise Binary Object Representation)编码,不同语言的 CBOR 库实现存在微妙差异。特别是 credentialPublicKey 的编码格式——有些库输出 DER,有些输出 COSE。

// ❌ 错误:手动解析 CBOR 容易出错
const publicKey = cbor.decodeFirstSync(credentialPublicKey);

// ✅ 正确:使用 @simplewebauthn 自动处理
import { verifyRegistrationResponse } from '@simplewebauthn/server';
// 库内部处理 CBOR 编解码,无需手动操作
const verification = await verifyRegistrationResponse({
  response: credential,
  expectedChallenge,
  expectedOrigin,
  expectedRPID: rpID,
});

坑 3:counter 更新不及时导致重放攻击风险

WebAuthn 的 counter 字段用于防止凭证重放攻击。每次认证成功后,服务端必须更新存储的 counter 值:

// ⚠️ 警告:不更新 counter 会导致重放攻击风险
// counter 检查:客户端返回的 counter 必须大于服务端存储的值
if (verification.verified) {
  passkey.counter = verification.authenticationInfo.newCounter;
  await savePasskeyToDB(passkey); // 必须持久化
}

坑 4:iOS Safari 的特殊行为

iOS Safari 在某些场景下会将 Passkey 视为「安全密钥」而非「平台凭证」,导致弹出「使用附近的安全密钥」的提示。解决方案是在 authenticatorSelection 中明确指定 authenticatorAttachment: 'platform'

3.3 多平台 Passkey 同步策略

Passkeys 的最大优势之一是跨设备同步,但不同平台的同步机制不同:

平台 同步服务 覆盖设备 特殊限制
Apple iCloud Keychain iPhone/iPad/Mac/Apple TV 需开启双重认证
Google Google Password Manager Android/Chrome/ChromeOS 需登录 Google 账号
Microsoft Windows Hello Windows 10/11 需 Microsoft 账号
1Password 1Password 全平台 需付费订阅
Bitwarden Bitwarden 全平台 开源免费

💡 **提示:**建议在用户注册 Passkey 时,引导用户确认其设备已登录 iCloud/Google 账号且开启了密码同步。否则设备丢失后 Passkey 将无法恢复。

3.4 安全审计清单

部署 Passkeys 前,请逐项检查以下安全配置:

  • ✅ HTTPS 强制启用(WebAuthn 的硬性要求)
  • ✅ RP ID 设置为实际域名(不要用 localhost
  • ✅ Challenge 使用密码学安全随机数生成(至少 16 字节)
  • ✅ Challenge 设置合理的过期时间(建议 5 分钟)
  • ✅ Counter 值持久化并正确更新
  • ✅ Origin 验证包含协议、域名和端口
  • ✅ 用户验证策略(User Verification)设置为 required
  • ✅ 凭证存储使用 base64url 编码(不是 base64)
  • ❌ 不要在 URL 中传递 challenge 或 credential
  • ❌ 不要跳过 certificate chain 验证(生产环境)

💡 总结与行动建议

Passkeys 不是未来的技术概念,而是已经落地的生产级认证方案。2026 年,全球 Passkey 注册量已超过 15 亿,主流框架(Next.js、Nuxt、Express)都有成熟的 WebAuthn 库支持。

行动建议:

  1. 立即开始:在现有系统中添加 Passkey 作为可选登录方式,不要等用户要求
  2. 渐进迁移:保持密码登录,用 Banner 引导用户升级到 Passkey
  3. 优先移动端:移动端的生物识别体验最好,是推广 Passkey 的最佳入口
  4. 监控指标:跟踪 Passkey 注册率、认证成功率、设备覆盖率

相关工具推荐:

📚 相关文章