WebAuthn 与 Passkey:告别密码时代的无密码认证完整实战
前端工程(更新于 2026年6月2日)
密码已死,Passkey 当立
密码的根本问题:密码是共享秘密——服务器和用户都知道,任何一方泄露都不安全。
| 问题 | 影响 |
|---|---|
| 弱密码 | 81% 的数据泄露源于弱密码 |
| 密码复用 | 一个泄露,全部沦陷 |
| 钓鱼攻击 | 用户无法区分真假网站 |
| 数据库泄露 | 服务器存储的哈希可能被破解 |
| MFA 短信劫持 | SIM swap 攻击 |
Passkey(WebAuthn/FIDO2) 用公钥加密替代密码:
密码模式:用户 → [密码] → 服务器(存储密码哈希)
Passkey模式:用户 → [签名] → 服务器(仅存公钥)
公钥无法反推私钥,服务器被黑也无法冒充用户。
WebAuthn 协议流程
注册流程
1. 用户点击"注册 Passkey"
2. 服务器生成 challenge → 发送到前端
3. 浏览器调用 authenticator → 用户验证(指纹/Face ID/PIN)
4. Authenticator 生成密钥对 → 返回公钥 + 签名
5. 前端发送公钥 + 签名到服务器
6. 服务器验证签名 → 存储公钥(credential)
认证流程
1. 用户点击"用 Passkey 登录"
2. 服务器生成 challenge → 发送到前端
3. 浏览器调用 authenticator → 用户验证
4. Authenticator 用私钥签名 challenge → 返回签名
5. 前端发送签名到服务器
6. 服务器用公钥验证签名 → 认证成功
完整代码实现
服务端:注册开始
// POST /api/webauthn/register/start
import { generateRegistrationOptions } from '@simplewebauthn/server';
export async function registerStart(userId: string, username: string) {
const options = await generateRegistrationOptions({
rpID: 'example.com',
rpName: '工具库',
userID: userId,
userName: username,
attestationType: 'none',
authenticatorSelection: {
authenticatorAttachment: 'platform', // 优先平台认证器
residentKey: 'required', // 可发现凭证(Passkey)
userVerification: 'preferred',
},
});
// 存储 challenge 供后续验证
await storeChallenge(userId, options.challenge);
return options;
}
前端:注册
import { startRegistration } from '@simplewebauthn/browser';
async function registerPasskey() {
try {
// 1. 从服务器获取注册选项
const options = await fetch('/api/webauthn/register/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id, username: user.name }),
}).then(r => r.json());
// 2. 调用浏览器 API 注册
const credential = await startRegistration({ optionsJSON: options });
// 3. 发送凭证到服务器验证
const result = await fetch('/api/webauthn/register/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ userId: user.id, credential }),
}).then(r => r.json());
if (result.verified) {
showToast('Passkey 注册成功!');
}
} catch (error) {
if (error.name === 'NotAllowedError') {
showToast('用户取消了操作');
}
}
}
服务端:注册完成
import { verifyRegistrationResponse } from '@simplewebauthn/server';
export async function registerFinish(userId: string, credential: any) {
const challenge = await getChallenge(userId);
const verification = await verifyRegistrationResponse({
response: credential,
expectedChallenge: challenge,
expectedOrigin: 'https://example.com',
expectedRPID: 'example.com',
});
if (verification.verified) {
// 存储凭证(公钥)
await storeCredential(userId, {
id: verification.registrationInfo?.credentialID,
publicKey: verification.registrationInfo?.credentialPublicKey,
counter: verification.registrationInfo?.counter,
deviceType: verification.registrationInfo?.credentialDeviceType,
});
}
return { verified: verification.verified };
}
前端:认证
import { startAuthentication } from '@simplewebauthn/browser';
async function authenticateWithPasskey() {
try {
// 1. 获取认证选项
const options = await fetch('/api/webauthn/auth/start', {
method: 'POST',
}).then(r => r.json());
// 2. 调用浏览器 API 认证
const credential = await startAuthentication({ optionsJSON: options });
// 3. 验证签名
const result = await fetch('/api/webauthn/auth/finish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ credential }),
}).then(r => r.json());
if (result.verified) {
redirect('/dashboard');
}
} catch (error) {
if (error.name === 'NotAllowedError') {
showToast('认证被取消');
}
}
}
平台认证器 vs 漫游认证器
| 维度 | 平台认证器 | 漫游认证器 |
|---|---|---|
| 示例 | Touch ID, Face ID, Windows Hello | YubiKey, Titan Key |
| 存储 | 设备内 TPM/Secure Enclave | 硬件安全芯片 |
| 跨设备 | iCloud/Google 同步 | 需物理携带 |
| 成本 | 免费 | $25-70 |
| 用户体验 | 指纹/面容,无感 | 插入/触碰 |
| 丢失风险 | 设备丢失 | 钥匙丢失 |
Passkey 同步机制
Apple iCloud Keychain:
iPhone 注册 Passkey → iCloud 同步 → Mac/iPad 自动可用
Google Password Manager:
Android 注册 Passkey → Google 同步 → Chrome 全平台可用
Windows Hello:
本地 TPM 存储 → 不跨设备同步
→ 建议同时注册平台 + 漫游 Passkey
降级策略:渐进式部署
function AuthForm() {
const [supportsPasskey, setSupportsPasskey] = useState(false);
useEffect(() => {
// 检测 WebAuthn 支持
setSupportsPasskey(
window.PublicKeyCredential !== undefined &&
typeof window.PublicKeyCredential === 'function'
);
}, []);
return (
<form>
{supportsPasskey && (
<Button onClick={authenticateWithPasskey}>
用 Passkey 登录
</Button>
)}
{/* 传统密码作为降级 */}
<Input type="email" placeholder="邮箱" />
<Input type="password" placeholder="密码" />
<Button type="submit">登录</Button>
</form>
);
}
条件 UI:自动检测 Passkey
// 检测用户是否有已注册的 Passkey
const isConditionalAvailable = await PublicKeyCredential.isConditionalMediationAvailable();
if (isConditionalAvailable) {
// 浏览器会在密码框自动显示 Passkey 选项
const credential = await startAuthentication({
optionsJSON: options,
useAutocomplete: true, // 启用条件 UI
});
}
<!-- 密码框会自动显示 Passkey 选项 -->
<input
type="password"
autocomplete="webauthn"
placeholder="密码或 Passkey"
/>
安全考量
1. Challenge 的安全性
// ✅ Challenge 必须是服务器生成的随机数
const challenge = crypto.randomUUID();
// ❌ 绝不要用固定值或时间戳
const badChallenge = Date.now().toString();
2. Origin 验证
// ✅ 严格验证 Origin,防止钓鱼
const expectedOrigin = 'https://example.com';
// ❌ 不要用通配符
const badOrigin = '*';
3. 凭证计数器
// 检测克隆凭证
if (newCounter <= storedCounter) {
// 凭证可能被克隆!
throw new Error('Possible cloned authenticator');
}
4. 多凭证策略
推荐:每个用户至少注册 2 个 Passkey
- 1 个平台 Passkey(日常使用)
- 1 个漫游 Passkey 或备用 Passkey(恢复用)
浏览器与平台支持
| 平台 | Passkey 支持 | 同步 | 条件 UI |
|---|---|---|---|
| Chrome (Windows/macOS) | ✅ | ✅ | |
| Safari (macOS/iOS) | ✅ | iCloud | ✅ |
| Firefox | ✅ | 无 | ❌ |
| Edge | ✅ | ✅ | |
| Android Chrome | ✅ | ✅ | |
| iOS Safari | ✅ | iCloud | ✅ |
总结
Passkey 是密码的终结者——基于公钥加密,抗钓鱼、抗泄露、抗重放。2026 年所有主流浏览器和操作系统均已支持,是时候在生产环境部署了。使用 @simplewebauthn 库可以快速集成,配合条件 UI 让用户无感切换。记住降级策略:Passkey 优先,密码兜底,渐进增强。
本站提供浏览器本地工具,免注册即可试用 →
#WebAuthn#Passkey#无密码认证#FIDO2#生物识别