WebAuthn 无密码认证与 Passkey 实践

系统讲解 WebAuthn 无密码认证与 Passkey 通行密钥:FIDO2 协议栈、公钥凭证注册与认证流程、对抗钓鱼与中间人攻击的原理、防重放与来源绑定,以及落地工程的用户体验、恢复机制与多设备同步方案。

导语:把“秘密”从服务器移到用户手里的硬件

密码的问题根源在于"服务器知道秘密":数据库泄露就是凭证泄露,钓鱼页面也能骗走密码。无密码认证的思路是不再共享秘密——用户持有的是一把私钥,服务器只保存公钥。WebAuthn 把这个构想变成浏览器原生标准,Passkey 让它在多设备间可同步、可恢复。

一句话总结: WebAuthn = 浏览器原生公钥认证协议;Passkey = WebAuthn 凭证的可同步形态。服务器不再存储任何可被冒充的共享秘密,钓鱼与中间人攻击自然失效。


1. 从密码到公钥凭证的演进

1.1 密码认证的固有缺陷

缺陷表现
服务器存储秘密数据库泄露 = 凭证泄露
密码可被钓鱼伪造页面骗取密码
密码可被猜测弱密码、撞库、喷洒
密码可被复用一处泄露牵连多处
无法绑定来源密码在哪个站用都一样

1.2 公钥凭证如何破解这些缺陷

注册时:
  用户的认证器(安全密钥/手机/系统钥匙串)生成一对密钥
  私钥:永不出设备,只在本机签名
  公钥:发给服务器保存

登录时:
  服务器给一个随机挑战(challenge)
  认证器用私钥对挑战签名
  服务器用公钥验签,确认"私钥确实在用户手里"

对比密码:
  密码:服务器存"能冒充你的东西",泄露即被盗
  公钥:服务器只存"能验证你的东西",泄露也冒充不了

一句话总结: 公钥认证把风险从"服务器存秘密"转移到"私钥永不出设备"——数据库泄露不再等于账号被盗,这是无密码最根本的价值。


2. FIDO2 协议栈与核心概念

2.1 FIDO2 与 WebAuthn 及 CTAP2

组件作用
WebAuthn浏览器/平台的标准 API,与 Relying Party 交互
CTAP2浏览器与外部认证器(USB/蓝牙安全密钥)的传输协议
认证器生成与保管密钥的设备:平台认证器(手机)或漫游认证器
Relying Party依赖方,即你的 Web 服务
协议分层(示意):

  Web 应用(RP) ↔ WebAuthn API ↔ 浏览器
                                     │
                              CTAP2/平台层
                                     │
                              认证器(私钥保管处)

公钥凭证注册与认证都在这条链上完成,
整个过程中服务器始终只接触公钥与签名结果。

2.2 关键数据结构 PublicKeyCredential

注册时产生:
  id            → 凭证 ID(对应认证器里那把私钥)
  response.attestationObject → 注册信息(含公钥、认证器元数据)
  response.clientDataJSON    → 包含来源 origin、挑战 challenge

登录时产生:
  id            → 凭证 ID
  response.authenticatorData → 认证器输出(含签名计数、来源)
  response.signature        → 私钥对挑战的签名
  response.clientDataJSON   → 包含 origin 与挑战

一句话总结: FIDO2 是一套完整协议栈,WebAuthn 负责浏览器侧,CTAP2 负责外部认证器;所有结构都以"公钥 + 签名"为中心,服务器永不接触私钥。


3. 注册流程详解

3.1 服务端发起注册

// 服务端生成注册选项(伪代码)
function registrationOptions(user) {
  return {
    challenge: generateRandomChallenge(),      // 一次性随机挑战
    rp: { id: "example.com", name: "示例站" },  // 依赖方标识
    user: {
      id: encodeUserId(user.id),               // 用户唯一标识
      name: user.username,
      displayName: user.displayName
    },
    pubKeyCredParams: [
      { type: "public-key", alg: -7 },         // ES256
      { type: "public-key", alg: -257 }        // RS256
    ],
    timeout: 60000,
    authenticatorSelection: {
      residentKey: "preferred",                // 是否需要可发现凭证
      userVerification: "preferred"            // 是否要求用户验证(指纹等)
    }
  };
}

3.2 前端调用创建凭证

// 浏览器侧注册
const options = await fetchRegisterOptions();
const publicKey = preparePublicKeyOptions(options);

const credential = await navigator.credentials.create({
  publicKey: publicKey
});

// 把结果交给服务端验签与存储
await fetch('/api/webauthn/register/verify', {
  method: 'POST',
  body: JSON.stringify({
    id: credential.id,
    rawId: arrayToBase64(credential.rawId),
    type: credential.type,
    response: {
      attestationObject: arrayToBase64(credential.response.attestationObject),
      clientDataJSON: arrayToBase64(credential.response.clientDataJSON)
    }
  })
});

3.3 服务端验证要点

注册验证必须检查:
  ① clientDataJSON 里的 origin 必须等于你声明的站点来源
  ② challenge 必须等于刚才发的那一个,且一次性
  ③ 解析 attestationObject,取出公钥与凭证 ID
  ④ 按安全级别决定是否验证"认证器证明"(attestation)
  ⑤ 存储:凭证 ID + 公钥 + 签名计数器(初始值)
常见坑:
  · 忘记校验 origin/challenge → 凭证可被跨站注册
  · 挑战可复用 → 重放攻击
  · 公钥不校验算法白名单 → 可能接受弱算法

一句话总结: 注册流程的关键是服务端严格验证 origin 与 challenge,并把公钥、凭证 ID、签名计数器安全存储——这步把关不严,后续认证流程的信任就无从谈起。


4. 认证流程详解

4.1 服务端发起认证

// 服务端生成认证选项(伪代码)
function authenticationOptions(user) {
  return {
    challenge: generateRandomChallenge(),
    rpId: "example.com",
    allowCredentials: [                       // 允许的凭证白名单
      { id: getCredentialId(user), type: "public-key" }
    ],
    userVerification: "preferred",
    timeout: 60000
  };
}

4.2 前端调用获取断言

// 浏览器侧认证
const options = await fetchAuthOptions();
const credential = await navigator.credentials.get({
  publicKey: prepareAuthOptions(options)
});

await fetch('/api/webauthn/login/verify', {
  method: 'POST',
  body: JSON.stringify({
    id: credential.id,
    type: credential.type,
    response: {
      authenticatorData: arrayToBase64(credential.response.authenticatorData),
      signature: arrayToBase64(credential.response.signature),
      clientDataJSON: arrayToBase64(credential.response.clientDataJSON)
    }
  })
});

4.3 服务端验签流程

认证验证必须检查(按顺序):
  ① 凭证 ID 是否存在于库中
  ② clientDataJSON 的 origin 与 challenge 校验
  ③ 从 authenticatorData 解析来源 rpIdHash,必须等于你的站点
  ④ 用存储的公钥验证 signature
  ⑤ 检查签名计数器:新计数必须大于旧计数(防克隆)

一次性挑战 + 签名:即使攻击者截获认证响应,
也无法重放,因为 challenge 已失效、origin 不匹配。

一句话总结: 认证流程本质是**“挑战 + 私钥签名 + 公钥验签”**;origin 绑定杀死钓鱼,一次性 challenge 杀死重放,签名计数器防克隆——三重校验缺一不可。


5. 对抗钓鱼与中间人攻击

5.1 为什么钓鱼拿不到有用的凭证

钓鱼页面伪造 example.com 的登录页,诱导用户"验证":

  真 WebAuthn:
    认证器签名的是"挑战 + 真实 origin(example.com)"
    钓鱼页 origin 是 attacker.com,验签必然失败
    私钥绑定站点,绝不替 attacker.com 签名

对比密码:
    钓鱼页只需拿到密码字符串,到真站就能用
    WebAuthn 的凭证永远绑死一个 rpId,跨站即失效

5.2 对抗中间人 MITM

攻击方式WebAuthn 为什么能防
劫持 DNS 指向假站origin 校验让假站验签失败
中间人改请求签名覆盖 origin 与 challenge,篡改即失效
重放认证响应一次性挑战过期,重放被拒
证书伪造浏览器通道 + 来源绑定双重保护

5.3 仍需注意的边界

· 恶意软件本地键盘记录:键盘记录器拿不到私钥,但可能截获其他输入
· 用户恶意授权:社会工程诱骗用户"亲自"在真站签名,仍需用户意识
· 认证器自身安全:受信任平台与设备须保持系统更新
· 恢复过程:找回流程若退化为"邮箱 + 密码",仍是攻击入口

一句话总结: WebAuthn 的**来源绑定(origin)+ 一次性挑战(challenge)**从协议层面让钓鱼与中间人失效;但社会工程、设备本身的安全,仍要靠用户教育与端侧防护补齐。


6. 落地工程的用户体验与恢复

6.1 体验设计要点

要点做法
渐进增强先支持,再推荐;不强制一步到位
登录默认有 Passkey 优先 Passkey,无则回落
生物识别平台认证器体验最优(指纹/面容)
失败降级认证失败给出清晰提示与替代路径
一次性注册注册成功即引导绑定备用方式

6.2 恢复与备用凭证策略

无密码最大的工程风险是"用户换了设备就登不进去"。
恢复策略:
  ① 多凭证:同一账号绑定多个认证器(手机 + 安全密钥)
  ② 同步通行密钥:用系统云同步(iCloud/Google)保留 Passkey
  ③ 备用因子:保留 TOTP/恢复码作为降级手段
  ④ 可验证的找回流程:找回必须经过同等强度的验证
提示:
  · 不要设计"通过密码重置"这种与无密码背道而驰的后门
  · 恢复码一次性、加密存储,与凭证强度匹配

6.3 一个落地校验的检查清单

□ origin 校验严格,站点来源白名单
□ challenge 一次性、过期短
□ 签名计数器持久化并递增校验
□ 支持多凭证管理与展示
□ 提供备用登录途径与恢复流程
□ 日志审计:认证成功/失败、凭证变更留痕
□ 合规:生物识别数据本地处理,不入服务端

一句话总结: 落地难点不在协议,而在体验与恢复——把多凭证、Passkey 同步、备用因子设计好,用户才愿意放弃密码;任何"方便的重置后门"都会重新打开钓鱼大门。


7. 多设备同步与兼容性

7.1 设备同步的两种形态

形态说明优点局限
漫游认证器USB/蓝牙安全密钥随身带强隔离、跨设备需要额外硬件
平台认证器 + 同步手机系统同步 Passkey体验好、跨设备依赖生态同步

7.2 Passkey 同步的信任模型

Passkey 同步(如 iCloud 钥匙串、Google 密码管理器):
  私钥在设备上生成,通过端到端加密同步到同一生态的其他设备
  服务器仍是只存公钥,同步只在用户自己的生态内发生

企业/高安全场景:
  可关闭"同步 Passkey",强制使用独立漫游密钥
  用策略控制:是否允许可发现凭证、是否要求用户验证
  通过强制验证(userVerification: required)保证"人在设备前"

7.3 兼容性现实

· 平台覆盖:主流浏览器均已支持 WebAuthn,但历史版本有差异
· 降级路径:老浏览器/老设备保留密码或 TOTP 兜底
· 证书方案:内部系统可借助 FIDO2 设备实现无密码桌面登录
· 迁移成本:从密码迁移建议"先并跑、再逐步收紧"

一句话总结: 多设备同步让 Passkey 走向大众,也带来生态绑定与信任模型选择——高安全场景关同步、强制验证;一般场景享受同步便利,但要保留降级路径。


8. 总结

环节关键动作
凭证私钥永不出设备,服务器只存公钥
注册严格校验 origin 与一次性挑战
认证验签 + 签名计数器 + 来源绑定
抗钓鱼origin 绑定让伪造站点验签失败
落地多凭证、Passkey 同步、备用因子
兼容老设备降级路径、渐进迁移

一句话记住:无密码不是"消灭口令",而是"消灭服务器上的秘密"——把信任从可泄露的共享秘密,换成永远不离开用户设备的私钥,钓鱼与中间人的土壤自然消失。配上良好的体验与恢复设计,Passkey 才能从"酷炫"变成"真正好用"。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「安全」更多文章

  1. 暴力破解防御与 MFA 加固
  2. 邮件安全与钓鱼防护实战
  3. 勒索软件防御与应急恢复