WebAuthn 与 Passkey:无密码认证
密码是最大的安全漏洞——弱密码、重用、钓鱼、泄露。Passkey(WebAuthn)用设备生物识别替代密码,安全又无感。本文讲清 WebAuthn 的原理(公私钥 + 设备认证)、Passkey 注册与验证流程、浏览器/平台支持现状,以及给你的产品加 Passkey 的落地路径。
本文要点
- 主题聚焦:React、DevOps
- 适合人群:独立开发者、全栈工程师、AI 应用开发者
- 核心价值:快速理解背景、实践路径、常见误区和工程建议
如果你做过登录系统,大概率在半夜被一条告警吵醒过:某个账号在异地登录、撞库攻击正在打你的 /login、又或者一个用户哭诉账号被盗。回头一看日志,绝大多数事故的根因都指向同一个东西——密码。
密码是一个诞生于上世纪六十年代的设计,却被要求承担 2026 年互联网的全部身份安全。它早就扛不住了。这一篇我们聊清楚:为什么密码是最大的安全漏洞,以及 Passkey(WebAuthn)如何用「设备 + 公私钥」把密码从登录流程里彻底干掉。
一、密码的系统性问题
先承认一个事实:密码作为一种认证因子,从原理上就是 broken 的。
它的问题不是「用户设置了弱密码」这么简单,而是一连串相互纠缠的系统性缺陷:
- 认知负担:一个正常人有几十个账号,不可能为每个账号记住一个 16 位随机串。于是要么用弱密码(
abc123456),要么重用。 - 重用 → 撞库:用户在某个小论坛用了和你银行一样的密码。小论坛一脱库(这种事每周都在发生),你的银行账号就成了待宰的羔羊。
- 钓鱼:密码是「可传递的凭证」——用户在
g00gle.com输入的密码,和真 Google 拿到的密码是一模一样的字符串。攻击者只要骗用户在假页面敲一次,游戏就结束了。 - 服务端泄露:哪怕你用了 bcrypt/scrypt/argon2,密码哈希依然是高价值目标。GPU 算力越来越便宜,离线爆破的门槛越来越低。
- 体验差:强制要求大小写+数字+符号,定期改密,短信验证码——这些都是在给一个本就该被淘汰的方案打补丁。
MFA(多因子)能缓解,但短信验证码能被 SIM 劫持,TOTP 需要用户手动抄 6 位数字,体验依然糟糕。真正的问题在于:密码这个东西,根本就不该存在。
Passkey 就是这个问题的答案。
二、WebAuthn 原理:公私钥 + 设备认证
WebAuthn(Web Authentication)是 W3C 标准,核心思想一句话:用非对称加密替代共享密钥。
整个体系建立在两个事实上:
- 每个账号在每个网站上,都有一对独立的公私钥;
- 私钥永远不离开用户的设备,服务端只存公钥。
这就从根本上消灭了上面所有问题。私钥不在服务端 → 服务端脱库拿不到能登录的东西;私钥不出设备 → 钓鱼网站拿不到私钥;每个站点独立密钥 → 撞库无从谈起。
私钥存在哪里? 现代设备的硬件安全区: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,后面会讲)。
后端:生成注册参数
// 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,降低隐私泄漏与实现复杂度
};
}
前端:调用生物识别 + 拿到公钥
// 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 选择 + 签名
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
不要搞「大爆炸」式迁移。正确的姿势是 并存 + 渐进:
- 先并存,不要替换。在现有密码登录页加一个「使用 Passkey 登录」按钮。已登录用户在账户设置里看到「为这个账号添加 Passkey」。让愿意尝鲜的用户先体验,这帮人会成为你的内测布道者。
- 注册 Passkey 的时机:用户刚登录成功时引导添加最自然,因为此时你已经确认了身份,可以安全地把公钥绑到这个账号上。不要在用户尚未认证时让他凭空创建 Passkey。
- 登录页 UI 改造:把 Passkey 按钮做成默认推荐项——「使用 Face ID 登录」比「使用密码登录」放得更显眼。行为经济学告诉我们,默认选项的力量远大于说教。
- 库的选择:Node.js 后端强烈推荐 SimpleWebAuthn(
@simplewebauthn/server+@simplewebauthn/browser)。它把上面那些 base64url 转换、challenge 校验、签名验证全包了,API 干净到让人想哭。同样的,Go 有go-webauthn,Python 有fido2库,PHP 有web-auth/webauthn-framework。 - 数据库改动:给用户加一张
passkeys表(user_id、credential_id、public_key、sign_count、created_at、device_label),一个用户可以有多条(多设备)。公钥是公开的,不用加密存——非对称加密的妙处就在这里。 - 过渡策略:观察一段时间,当某个用户连续 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