2025 年底,Google 宣布所有 Android 设备默认启用 Passkeys,GitHub 的 Passkeys 登录占比已突破 40%。Passkeys 不是概念验证,而是正在取代密码的主流认证方案。如果你还在用 bcrypt 哈希 + session 那套,是时候了解 WebAuthn 了。
本文不讲空话,直接从协议原理、前后端代码实现、安全威胁模型三个维度拆解 Passkeys,给出一套可直接部署的 Node.js + 浏览器端完整方案。
🔐 一、Passkeys 核心原理:为什么它比密码安全
公钥密码学在浏览器中的落地
Passkeys 基于 W3C 的 WebAuthn(Web Authentication)标准和 FIDO2 协议。核心机制很简单:注册时,设备生成一对公私钥(非对称加密),私钥永远不离开设备,公钥发送给服务器存储。认证时,服务器发送一个随机挑战(Challenge),设备用私钥签名,服务器用公钥验证。
📌 **记住:**Passkeys 的本质是非对称签名认证,不是加密。服务器永远不接触私钥,所以数据库泄露也不会暴露用户凭据。
这和 SSH key 的原理完全一致,但 WebAuthn 增加了两个关键约束:
- 起源绑定(Origin Binding):签名中包含域名,钓鱼网站无法复用
- 用户验证(User Verification):要求生物识别或 PIN,防止设备被盗后滥用
三种认证模式对比
| 模式 | 存储位置 | 跨设备 | 脱机可用 | 安全等级 |
|---|---|---|---|---|
| 密码(Password) | 服务器数据库 | ✅ | ❌ | 低(可被钓鱼/暴力破解) |
| OTP / TOTP | 共享密钥 | ✅ | ✅ | 中(可被中间人攻击) |
| Passkeys(平台) | 设备安全芯片 | ❌ | ✅ | 高(防钓鱼、防泄露) |
| Passkeys(漫游) | 外接密钥/手机 | ✅ | ✅ | 高(防钓鱼、防泄露) |
⚠️ **警告:**Passkeys 的「平台认证器」(如 Face ID、指纹)绑定单一设备。如果设备丢失且没有备份方案,用户将永久锁定。必须设计账户恢复流程。
🚀 二、完整实现:Node.js 后端 + 浏览器前端
依赖与环境
后端使用 @simplewebauthn/server 库,它是 WebAuthn 规范最完整的 Node.js 实现。前端使用浏览器原生 navigator.credentials API。
# 后端依赖
npm install @simplewebauthn/server @simplewebauthn/browser express
第一步:注册流程 — 生成挑战
服务器生成挑战值(Challenge),发给前端调用设备的认证器(Authenticator)生成密钥对。
// server/register-challenge.js
import { generateRegistrationOptions } from '@simplewebauthn/server';
// 模拟数据库(生产环境用 PostgreSQL / Redis)
const userDB = new Map();
const challengeStore = new Map();
async function getRegistrationChallenge(userId, username) {
// 获取该用户已注册的凭据,用于排除重复注册
const existingCredentials = userDB.get(userId)?.credentials || [];
const options = await generateRegistrationOptions({
rpName: 'JSJSON 开发者工具箱',
rpID: 'jsjson.com',
userName: username,
// 用户唯一标识(不要用邮箱,用 UUID)
userID: new TextEncoder().encode(userId),
userDisplayName: username,
timeout: 60000, // 60 秒超时
attestationType: 'none', // 不需要设备证明,减少隐私泄露
excludeCredentials: existingCredentials.map(cred => ({
id: cred.credentialID,
type: 'public-key',
})),
authenticatorSelection: {
// 优先使用平台认证器(指纹/面容),无则用漫游密钥
authenticatorAttachment: 'platform',
requireResidentKey: true, // 无密码登录必须为 true
residentKey: 'required',
userVerification: 'preferred',
},
// 支持的算法:ES256 (P-256) 和 RS256
supportedAlgorithmIDs: [-7, -257],
});
// 存储挑战值,验证时必须匹配
challengeStore.set(userId, options.challenge);
return options;
}
💡 提示:
requireResidentKey: true是实现「无密码登录」的关键。启用后,认证器会存储用户信息,后续登录不需要用户输入用户名。
第二步:注册流程 — 验证响应
前端调用 navigator.credentials.create() 后,将响应发回服务器验证。
// server/register-verify.js
import { verifyRegistrationResponse } from '@simplewebauthn/server';
async function verifyRegistration(userId, credential) {
const expectedChallenge = challengeStore.get(userId);
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge,
expectedOrigin: 'https://jsjson.com',
expectedRPID: 'jsjson.com',
});
if (verification.verified && verification.registrationInfo) {
const { credentialPublicKey, credentialID, counter } = verification.registrationInfo;
// 存储凭据信息(生产环境存数据库)
const user = userDB.get(userId) || { credentials: [] };
user.credentials.push({
credentialID,
credentialPublicKey,
counter, // 签名计数器,用于检测克隆
createdAt: new Date(),
});
userDB.set(userId, user);
// 清除已使用的挑战值
challengeStore.delete(userId);
}
return verification.verified;
}
⚠️ **警告:**生产环境中,挑战值(Challenge)必须存在服务端 session 或 Redis 中,带过期时间。绝对不能信任客户端返回的 Challenge。
第三步:登录流程 — 无密码认证
登录时,服务器发送随机 Challenge,设备用私钥签名。
// server/login.js
import {
generateAuthenticationOptions,
verifyAuthenticationResponse,
} from '@simplewebauthn/server';
async function getLoginChallenge(userId) {
const user = userDB.get(userId);
if (!user) throw new Error('用户不存在');
const options = await generateAuthenticationOptions({
rpID: 'jsjson.com',
userVerification: 'preferred',
timeout: 60000,
allowCredentials: user.credentials.map(cred => ({
id: cred.credentialID,
type: 'public-key',
})),
});
challengeStore.set(userId, options.challenge);
return options;
}
async function verifyLogin(userId, credential) {
const user = userDB.get(userId);
const expectedChallenge = challengeStore.get(userId);
// 找到匹配的凭据
const storedCred = user.credentials.find(
c => c.credentialID.toString() === credential.id
);
const verification = await verifyAuthenticationResponse({
response: credential,
expectedChallenge,
expectedOrigin: 'https://jsjson.com',
expectedRPID: 'jsjson.com',
credential: {
id: storedCred.credentialID,
publicKey: storedCred.credentialPublicKey,
counter: storedCred.counter,
},
});
if (verification.verified) {
// 更新计数器,防止克隆攻击
storedCred.counter = verification.authenticationInfo.newCounter;
challengeStore.delete(userId);
}
return verification.verified;
}
前端代码:调用浏览器 API
// client/passkeys.js
import {
startRegistration,
startAuthentication,
} from '@simplewebauthn/browser';
// 注册 Passkey
async function registerPasskey(userId, username) {
try {
// 1. 从服务器获取注册选项
const resp = await fetch('/api/passkey/register-challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, username }),
});
const options = await resp.json();
// 2. 调用设备认证器生成密钥对
const credential = await startRegistration(options);
// 3. 将凭据发回服务器验证
const verifyResp = await fetch('/api/passkey/register-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, credential }),
});
const result = await verifyResp.json();
console.log('注册成功:', result.verified);
return result.verified;
} catch (err) {
// 用户取消或设备不支持
if (err.name === 'NotAllowedError') {
console.warn('用户取消了 Passkey 注册');
} else if (err.name === 'SecurityError') {
console.error('安全错误:请确保使用 HTTPS');
}
throw err;
}
}
// 使用 Passkey 登录
async function loginWithPasskey(userId) {
try {
const resp = await fetch('/api/passkey/login-challenge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId }),
});
const options = await resp.json();
// 浏览器弹出认证器选择(指纹/面容/PIN)
const credential = await startAuthentication(options);
const verifyResp = await fetch('/api/passkey/login-verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId, credential }),
});
return await verifyResp.json();
} catch (err) {
if (err.name === 'NotAllowedError') {
console.warn('用户取消了认证');
}
throw err;
}
}
💡 三、生产环境的坑与避坑指南
坑 1:HTTPS 是硬性要求
WebAuthn API 只在安全上下文(Secure Context)中可用。localhost 可以开发测试,但任何非 https:// 的部署都会直接报 SecurityError。这意味着你不能在 HTTP 环境下做集成测试。
⚠️ **警告:**开发环境用
mkcert生成本地可信证书,不要跳过 HTTPS 测试。
# 生成本地自签名证书
mkcert -install
mkcert jsjson.local 127.0.0.1 ::1
# 输出 jsjson.local+2.pem 和 jsjson.local+2-key.pem
坑 2:跨设备登录的用户体验
Passkeys 的最大痛点是跨设备。用户在手机上注册了 Passkey,换电脑登录时怎么办?目前有三种方案:
| 方案 | 实现复杂度 | 用户体验 | 安全性 |
|---|---|---|---|
| 二维码 + BLE(Hybrid) | 中(浏览器自动支持) | 好(扫码即可) | 高 |
| 外接安全密钥(YubiKey) | 低 | 中(需要物理设备) | 最高 |
| 云同步(iCloud/Google) | 低(平台自动处理) | 最好 | 中(依赖平台安全) |
实际建议:优先依赖 iCloud Keychain 和 Google Password Manager 的云同步,同时保留外接密钥作为备选。90% 的用户会通过云同步自动获得跨设备能力。
坑 3:账户恢复方案
Passkeys 最大的运营风险:用户设备丢失后无法登录。必须设计恢复流程:
- 注册多个 Passkey:鼓励用户在手机 + 电脑 + 外接密钥上各注册一个
- 保留备用认证方式:TOTP 恢复码或备用邮箱
- 管理员重置:提供客服通道重置 Passkey
💡 **提示:**在注册页面明确提示用户「建议在多个设备上注册 Passkey」,并提供引导流程。这一步很多产品都忽略了。
坑 4:Counter 的正确使用
WebAuthn 规范中的 signCount 字段用于检测克隆攻击。如果连续两次认证的 counter 值相同或倒退,说明凭据可能被克隆。但实际中:
- Apple 的 Passkeys 实现不使用 counter(始终返回 0)
- Google 的实现也类似
- 只有硬件安全密钥(YubiKey)真正使用 counter
所以不要把 counter 异常当作硬性拒绝条件,记录日志并标记为可疑即可。
📊 四、性能与安全数据对比
| 指标 | 密码 + bcrypt | Passkeys (WebAuthn) |
|---|---|---|
| 注册耗时(用户操作) | 5-10 秒 | 3-5 秒(一次生物识别) |
| 登录耗时(用户操作) | 8-15 秒(输入+等待) | 2-3 秒(一次生物识别) |
| 服务端验证耗时 | 50-200ms(bcrypt) | 1-5ms(签名验证) |
| 钓鱼防御 | ❌ 完全无效 | ✅ 域名绑定,无法钓鱼 |
| 数据库泄露影响 | ❌ 密码哈希可被离线破解 | ✅ 只有公钥,无法逆推私钥 |
| 用户密码重置频率 | 高(平均 2-3 次/年) | 几乎为零 |
⚡ **关键结论:**Passkeys 在安全性、用户体验和服务端性能三个维度全面碾压密码认证。服务端验证从 bcrypt 的 50-200ms 降到 1-5ms,这是 40-100 倍的性能提升。
✅ 总结与落地建议
Passkeys 已经不是实验性技术,而是生产就绪的认证方案。对于新项目,建议直接采用 Passkeys 作为主认证方式,保留密码作为过渡方案。对于存量项目,可以先作为可选的第二认证方式逐步推进。
落地路径建议:
- ✅ 先在登录页添加「使用 Passkey 登录」按钮,与密码登录并存
- ✅ 注册时引导用户创建 Passkey,展示跨设备使用说明
- ✅ 设置多个 Passkey 的注册提醒,降低账户丢失风险
- ✅ 保留 TOTP / 恢复码作为账户恢复通道
- ❌ 不要一次性强制迁移,给用户适应时间
⚡ **关键结论:**Passkeys 的技术成熟度已经不是问题,真正的挑战在于用户教育和迁移策略。渐进式迁移(Progressive Enhancement)是最安全的落地方式。
相关工具与资源:
- 🔧 SimpleWebAuthn — 最成熟的 WebAuthn 库(Node.js + 浏览器端)
- 🔧 WebAuthn.io — 在线测试 WebAuthn 注册和认证流程
- 🔧 FIDO Alliance 测试套件 — 兼容性测试工具
- 📖 W3C WebAuthn Level 3 规范 — 权威协议文档
- 🔧 mkcert — 本地 HTTPS 证书生成工具