#React#DevOps

WebAuthn 与 Passkey:无密码认证

密码是最大的安全漏洞——弱密码、重用、钓鱼、泄露。Passkey(WebAuthn)用设备生物识别替代密码,安全又无感。本文讲清 WebAuthn 的原理(公私钥 + 设备认证)、Passkey 注册与验证流程、浏览器/平台支持现状,以及给你的产品加 Passkey 的落地路径。

本文要点

  • 主题聚焦:React、DevOps
  • 适合人群:独立开发者、全栈工程师、AI 应用开发者
  • 核心价值:快速理解背景、实践路径、常见误区和工程建议
作者 @admin·发布于 2026年4月11日·更新于 2026年7月23日

如果你做过登录系统,大概率在半夜被一条告警吵醒过:某个账号在异地登录、撞库攻击正在打你的 /login、又或者一个用户哭诉账号被盗。回头一看日志,绝大多数事故的根因都指向同一个东西——密码。

密码是一个诞生于上世纪六十年代的设计,却被要求承担 2026 年互联网的全部身份安全。它早就扛不住了。这一篇我们聊清楚:为什么密码是最大的安全漏洞,以及 Passkey(WebAuthn)如何用「设备 + 公私钥」把密码从登录流程里彻底干掉。

一、密码的系统性问题

先承认一个事实:密码作为一种认证因子,从原理上就是 broken 的。

它的问题不是「用户设置了弱密码」这么简单,而是一连串相互纠缠的系统性缺陷:

  • 认知负担:一个正常人有几十个账号,不可能为每个账号记住一个 16 位随机串。于是要么用弱密码(abc123456),要么重用。
  • 重用 → 撞库:用户在某个小论坛用了和你银行一样的密码。小论坛一脱库(这种事每周都在发生),你的银行账号就成了待宰的羔羊。
  • 钓鱼:密码是「可传递的凭证」——用户在 g00gle.com 输入的密码,和真 Google 拿到的密码是一模一样的字符串。攻击者只要骗用户在假页面敲一次,游戏就结束了。
  • 服务端泄露:哪怕你用了 bcrypt/scrypt/argon2,密码哈希依然是高价值目标。GPU 算力越来越便宜,离线爆破的门槛越来越低。
  • 体验差:强制要求大小写+数字+符号,定期改密,短信验证码——这些都是在给一个本就该被淘汰的方案打补丁。

MFA(多因子)能缓解,但短信验证码能被 SIM 劫持,TOTP 需要用户手动抄 6 位数字,体验依然糟糕。真正的问题在于:密码这个东西,根本就不该存在。

Passkey 就是这个问题的答案。

二、WebAuthn 原理:公私钥 + 设备认证

WebAuthn(Web Authentication)是 W3C 标准,核心思想一句话:用非对称加密替代共享密钥。

整个体系建立在两个事实上:

  1. 每个账号在每个网站上,都有一对独立的公私钥;
  2. 私钥永远不离开用户的设备,服务端只存公钥。

这就从根本上消灭了上面所有问题。私钥不在服务端 → 服务端脱库拿不到能登录的东西;私钥不出设备 → 钓鱼网站拿不到私钥;每个站点独立密钥 → 撞库无从谈起。

私钥存在哪里? 现代设备的硬件安全区:Mac 是 Secure Enclave,Windows 是 TPM 芯片,Android 是 Titan M / TrustZone,iOS 同样是 Secure Enclave。私钥被硬件保护,操作系统本身都读不出来,只能让硬件「代为签名」。

而让硬件真正签名的「用户验证」环节——指纹、Face ID、设备 PIN——是本地发生的,永远不会上传到服务端。你的指纹数据从来没有离开过手机,WebAuthn 只让设备告诉服务端一句「我刚刚本地验证过用户了,可信」。

认证流程的精髓:

  • 注册时,设备生成密钥对,公钥 + 一个 credential ID 发给服务端存起来;
  • 登录时,服务端发一个随机 challenge,设备用私钥对 challenge 签名,服务端用之前存的公钥验签。

验签通过 = 登录成功。整个过程没有一个字符的密码参与。

三、Passkey = WebAuthn 的「可同步」版

这里有个容易混淆的点:WebAuthn 是协议,Passkey 是产品形态。

早期的 WebAuthn 凭证是「设备绑定的」(device-bound credential)——私钥锁死在这一台设备上,丢了就丢了,换一台设备就得重新注册。企业安全喜欢这种强绑定,但普通用户根本受不了。

Passkey 是 Apple、Google、Microsoft 在 2022 年联合推动的概念,本质上是可跨设备同步的 WebAuthn 凭证:

  • Apple 设备之间通过 iCloud 钥匙串同步;
  • Android 设备通过 Google 密码管理器同步;
  • Windows 通过 Microsoft 账户同步。

于是用户在 iPhone 上注册的 Passkey,可以在 Mac、iPad 上直接用;换新手机,Passkey 跟着账户走,不用重新配对。这才是「无密码」该有的体验。

对开发者来说,好消息是:Passkey 用的还是 WebAuthn API,你写的代码不需要为了「Passkey」和「设备绑定的 WebAuthn」做区分。是不是可同步,是平台和用户系统层面决定的,你的服务端拿到的是同样格式的公钥。

更进一步,WebAuthn 还支持 hybrid transport:电脑上的网站可以直接调用你口袋里的手机来完成认证——手机弹个 Face ID,电脑上的网站就登录了。这就是真正的「跨设备无密码」。

四、注册流程:create() 拿到公钥

注册一个 Passkey,前端调 navigator.credentials.create(),后端发一组参数。下面是一个最小可跑的例子(实际生产建议用 SimpleWebAuthn,后面会讲)。

后端:生成注册参数

js
// server.js —— 生成 registration challenge
import { randomBytes } from 'node:crypto';

export function generateRegistrationOptions(user) {
  const challenge = randomBytes(32);
  // challenge 要存 session,验证时对照,且一次性、60s 过期
  return {
    challenge: challenge.toString('base64url'),
    rp: { name: 'CodeNodeX', id: 'codenodex.dev' }, // rp.id 绑定域名,防钓鱼
    user: {
      id: Buffer.from(user.id).toString('base64url'),
      name: user.email,
      displayName: user.name,
    },
    pubKeyCredParams: [
      { type: 'public-key', alg: -7 },  // ES256
      { type: 'public-key', alg: -257 }, // RS256
    ],
    authenticatorSelection: {
      authenticatorAttachment: 'platform', // 只要平台认证器(指纹/Face ID),不要 USB key
      userVerification: 'required',        // 必须本地生物识别
      residentKey: 'preferred',            // 可同步 → 即 Passkey
    },
    timeout: 60000,
    attestation: 'none', // 生产环境一般不用 attestation,降低隐私泄漏与实现复杂度
  };
}

前端:调用生物识别 + 拿到公钥

js
// client.js —— 触发 Face ID / 指纹,拿到公钥
async function registerPasskey(optionsFromServer) {
  // 注意:challenge / user.id 必须转成 BufferSource
  const publicKey = {
    ...optionsFromServer,
    challenge: base64urlToBuffer(optionsFromServer.challenge),
    user: {
      ...optionsFromServer.user,
      id: base64urlToBuffer(optionsFromServer.user.id),
    },
  };

  // 这一行会唤起系统的 Face ID / 指纹 / PIN 弹窗
  const credential = await navigator.credentials.create({ publicKey });

  // 拿到的 credential 包含公钥 + credential ID,发给后端存起来
  const payload = {
    id: credential.id,
    rawId: bufferToBase64url(credential.rawId),
    type: credential.type,
    response: {
      attestationObject: bufferToBase64url(credential.response.attestationObject),
      clientDataJSON: bufferToBase64url(credential.response.clientDataJSON),
    },
  };

  await fetch('/api/passkey/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });
}

后端:验证 + 落库

服务端要做几件事:校验 challenge 是否是刚才发出去的、origin 是不是自己的、rpId 是否匹配、userVerification 是否真的完成了。验证通过后,从 attestationObject 里解析出公钥,和 credential ID 一起存到库里——这就是这个用户在这台设备上的「凭证」。

这一步的坑在于 base64url ↔ ArrayBuffer 的来回转换,字段非常多,手写容易出错。这也是为什么我强烈建议直接用 SimpleWebAuthn。

五、验证流程:get() 签名验签

登录的时候,流程反过来:服务端发 challenge,设备用私钥签名,服务端验签。

前端:唤起 Passkey 选择 + 签名

js
async function loginWithPasskey() {
  // 让服务端发一个 challenge
  const { challenge } = await fetch('/api/passkey/login-options').then(r => r.json());

  const publicKey = {
    challenge: base64urlToBuffer(challenge),
    timeout: 60000,
    userVerification: 'required',
    // 不指定 allowCredentials → 让浏览器自动选出本站可用的 Passkey,真正的无密码体验
  };

  // 这一行在手机上弹 Face ID,在电脑上弹 Touch ID / Windows Hello
  const assertion = await navigator.credentials.get({ publicKey });

  const payload = {
    id: assertion.id,
    rawId: bufferToBase64url(assertion.rawId),
    type: assertion.type,
    response: {
      authenticatorData: bufferToBase64url(assertion.response.authenticatorData),
      clientDataJSON: bufferToBase64url(assertion.response.clientDataJSON),
      signature: bufferToBase64url(assertion.response.signature),
    },
  };

  // 发给后端验签,通过 → 颁发 session
  const res = await fetch('/api/passkey/login-verify', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(payload),
  });

  if (res.ok) {
    console.log('登录成功,全程没输入一个字符');
  }
}

注意上面注释那句:不指定 allowCredentials。这是 Passkey 体验的关键——浏览器会根据 rp.id(也就是你的域名)自动匹配本站的 Passkey,用户甚至不需要先输入用户名。点一下「使用 Passkey 登录」,Face ID 一扫,就进去了。

后端验签要用之前存的公钥校验 signature,同时验证 challenge 没被重放、origin 是可信的、signCount 是否异常(防克隆)。一旦验签通过,就给用户颁发 session,登录完成。整个过程没有密码、没有验证码、没有短信,但安全性比这些都高。

六、浏览器与平台支持现状

到 2026 年,你可以认为 Passkey 已经是「全面可用」的状态:

  • 浏览器:Chrome、Safari、Edge、Firefox 全部支持 WebAuthn Level 3。移动端 Safari 和 Chrome on Android 都已支持 Passkey 的创建与使用。
  • 平台:
    • iOS / iPadOS 16+、macOS Ventura+:通过 iCloud 钥匙串跨设备同步;
    • Android 9+:通过 Google 密码管理器同步,Chrome OS 共享;
    • Windows 10/11:通过 Windows Hello + Microsoft 账户同步。
  • 跨设备认证(hybrid):电脑上的网站可以扫码调起手机上的 Passkey,这个能力在主流浏览器里都已经落地。

简单说,95% 以上的现代设备用户都能用上 Passkey。你不再需要担心「这是不是太前沿了」——它已经不前沿了。Google、TikTok、GitHub、Best Buy 等都已经大规模上线。

七、落地路径:给现有系统加 Passkey

不要搞「大爆炸」式迁移。正确的姿势是 并存 + 渐进:

  1. 先并存,不要替换。在现有密码登录页加一个「使用 Passkey 登录」按钮。已登录用户在账户设置里看到「为这个账号添加 Passkey」。让愿意尝鲜的用户先体验,这帮人会成为你的内测布道者。
  2. 注册 Passkey 的时机:用户刚登录成功时引导添加最自然,因为此时你已经确认了身份,可以安全地把公钥绑到这个账号上。不要在用户尚未认证时让他凭空创建 Passkey。
  3. 登录页 UI 改造:把 Passkey 按钮做成默认推荐项——「使用 Face ID 登录」比「使用密码登录」放得更显眼。行为经济学告诉我们,默认选项的力量远大于说教。
  4. 库的选择:Node.js 后端强烈推荐 SimpleWebAuthn(@simplewebauthn/server + @simplewebauthn/browser)。它把上面那些 base64url 转换、challenge 校验、签名验证全包了,API 干净到让人想哭。同样的,Go 有 go-webauthn,Python 有 fido2 库,PHP 有 web-auth/webauthn-framework。
  5. 数据库改动:给用户加一张 passkeys 表(user_id、credential_id、public_key、sign_count、created_at、device_label),一个用户可以有多条(多设备)。公钥是公开的,不用加密存——非对称加密的妙处就在这里。
  6. 过渡策略:观察一段时间,当某个用户连续 N 次都用 Passkey 登录成功,可以悄悄提示「要不要关闭密码登录?」。但永远保留一个密码或备用通道作为后路。

给个判断:一个面向 C 端、有登录系统、用户大多在移动端的产品,Passkey 的 ROI 极高——既显著降低账号被盗率,又明显提升登录转化率(用户不用记密码了)。这种好事在安全领域并不多。

八、恢复策略:设备丢了怎么办

这是 Passkey 落地时被问得最多的问题,也是最容易被忽视的。

Passkey 的可同步性本身已经缓解了大部分问题——你的 Passkey 不在一台设备上,而在 iCloud / Google 账户里,换设备不丢。但仍然要考虑极端情况:

  • 至少留一个备用认证通道。账号被锁死的体验是灾难性的,没人愿意面对。可以是邮箱重置链接、可以是 TOTP、甚至可以保留密码作为兜底。不要把 Passkey 做成唯一因子。
  • 鼓励用户在多台设备上添加 Passkey。「添加这台设备的 Face ID」要做得很顺手,让用户在手机和电脑上各加一份,等于天然的冗余。
  • Account Recovery 流程要重设计。设备全丢的情况下,服务端要有一套基于邮箱验证 + 设备指纹 + 行为风险打分的恢复流程,不能简单粗暴地用 Passkey 一刀切。
  • 企业场景考虑设备绑定的 WebAuthn。如果你的合规要求私钥绝对不可离开公司设备,那就要禁用 residentKey: 'preferred',改用设备绑定的 WebAuthn + 企业发的硬件 key(YubiKey 等)。这牺牲了便利性,换来更强的物理控制。

记住一句话:认证系统的安全性,等于它恢复流程的脆弱程度。Passkey 把日常登录的安全性拉到了天花板,但恢复链路必须同时跟上,否则就是把一扇铁门装在了一扇纸糊的窗户上。

小结

  • 密码是上世纪的设计,撞库、钓鱼、泄露是它的结构性疾病,补丁打不动了。
  • WebAuthn 用「公私钥 + 设备硬件」把私钥锁在 Secure Enclave / TPM 里,服务端只存公钥,从原理上消灭了密码泄露与重放。
  • Passkey = 可同步的 WebAuthn,跨设备走 iCloud / Google 同步,让无密码体验真正落地。
  • 落地用 SimpleWebAuthn,Node 后端几十行就能跑通;数据库加一张 passkeys 表存公钥即可。
  • 永远保留备用认证通道,设备丢失的恢复流程比登录本身更重要。

金句:密码的本质缺陷,在于它是一个可以被复制的秘密;而 Passkey 的优雅之处,在于它让秘密永远只活在你的设备里。

Related

相关推荐