我们在密码安全系列讲过各种保护密码的方法:加盐哈希、慢哈希、Argon2…
但所有这些努力都在解决一个问题:密码泄露后怎么办。
有没有可能从根本上解决问题?让密码不存在?
这就是 Passkey。
密码的根本问题
密码为什么不安全?
密码的本质:一个"共享的秘密"
注册时:
你 → 服务器:"我的密码是 123456"
服务器存下来(哈希后)
登录时:
你 → 服务器:"我的密码是 123456"
服务器验证:哈希值匹配?
问题:
这个秘密必须"传输"和"存储"
传输时可能被窃听
存储时可能被拖库
密码的攻击面
┌────────────────────────────────────────────────────────────┐
│ 密码的攻击面 │
├────────────────────────────────────────────────────────────┤
│ │
│ 1. 钓鱼攻击 │
│ 假网站骗你输入密码 │
│ → 密码被盗 │
│ │
│ 2. 暴力破解 │
│ 不断尝试密码组合 │
│ → 弱密码被破解 │
│ │
│ 3. 撞库攻击 │
│ 用其他网站泄露的密码尝试 │
│ → 密码复用被利用 │
│ │
│ 4. 数据库泄露 │
│ 服务器被入侵 │
│ → 密码哈希被盗,可离线破解 │
│ │
│ 5. 社会工程学 │
│ 骗你说出密码 │
│ → 人是最弱的环节 │
│ │
│ 6. 键盘记录 / 屏幕录制 │
│ 恶意软件捕获输入 │
│ → 密码被窃取 │
│ │
└────────────────────────────────────────────────────────────┘
根本原因:密码是"知识因素"
只要"知道"密码就能登录
而"知道"这件事,无法被独占
公钥密码学:反转思路
核心思想
传统密码(对称):
你和服务器共享同一个秘密
谁有这个秘密,谁就能登录
公钥密码学(非对称):
你有私钥,服务器有公钥
私钥永远不离开你的设备
服务器用公钥验证你的身份
关键区别:
私钥不需要传输
私钥不需要让服务器知道
即使服务器被攻破,私钥也是安全的
认证流程
┌─────────────────────────────────────────────────────────────┐
│ 公钥认证流程 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 注册: │
│ 1. 设备生成密钥对(私钥 + 公钥) │
│ 2. 私钥存在设备的安全区域 │
│ 3. 公钥发给服务器保存 │
│ │
│ 登录: │
│ 1. 服务器发送随机挑战(Challenge) │
│ 2. 设备用私钥签名挑战 │
│ 3. 服务器用公钥验证签名 │
│ 4. 签名有效 → 认证成功 │
│ │
│ 安全性: │
│ ✓ 私钥从不离开设备 │
│ ✓ 服务器不存储任何敏感信息(公钥是公开的) │
│ ✓ 每次挑战不同,无法重放 │
│ ✓ 没有密码可以钓鱼 │
│ │
└─────────────────────────────────────────────────────────────┘
WebAuthn 和 FIDO2
Passkey 不是凭空出现的,它基于成熟的标准。
标准族谱
FIDO(Fast IDentity Online)联盟制定了无密码认证标准
FIDO2 = WebAuthn + CTAP
┌────────────────────────────────────────────────────────────┐
│ FIDO2 架构 │
├────────────────────────────────────────────────────────────┤
│ │
│ WebAuthn(W3C 标准) │
│ └─ 浏览器/应用 与 服务器 之间的 API │
│ └─ JavaScript API:navigator.credentials.create/get │
│ │
│ CTAP(Client to Authenticator Protocol) │
│ └─ 浏览器/应用 与 认证器 之间的协议 │
│ └─ 认证器:指纹、Face ID、YubiKey、手机... │
│ │
└────────────────────────────────────────────────────────────┘
Passkey = FIDO2 的用户友好品牌名
认证器类型
┌─────────────────┬────────────────────────────────────────────┐
│ 类型 │ 说明 │
├─────────────────┼────────────────────────────────────────────┤
│ 平台认证器 │ 内置于设备 │
│ (Platform) │ 如:MacBook 的 Touch ID,iPhone 的 Face ID │
│ │ 优点:方便,用户已有 │
│ │ 缺点:换设备需要迁移 │
├─────────────────┼────────────────────────────────────────────┤
│ 漫游认证器 │ 独立设备 │
│ (Roaming) │ 如:YubiKey,手机作为认证器 │
│ │ 优点:跨设备使用 │
│ │ 缺点:需要携带,可能丢失 │
├─────────────────┼────────────────────────────────────────────┤
│ 同步 Passkey │ 云端同步的 Passkey │
│ (Synced) │ 如:iCloud Keychain,Google Password Manager│
│ │ 优点:跨设备无缝使用 │
│ │ 缺点:依赖云服务商的安全性 │
└─────────────────┴────────────────────────────────────────────┘
WebAuthn 流程详解
注册流程(创建 Passkey)
┌──────┐ ┌─────────┐ ┌────────┐ ┌────────┐
│ 用户 │ │ 浏览器 │ │ 认证器 │ │ 服务器 │
└──┬───┘ └────┬────┘ └───┬────┘ └───┬────┘
│ │ │ │
│ 1. 点击注册 │ │ │
│────────────────>│ │ │
│ │ │ │
│ │ 2. 请求注册选项 │
│ │────────────────────────────────>│
│ │ │ │
│ │ 3. 返回 challenge + 配置 │
│ │<────────────────────────────────│
│ │ │ │
│ │ 4. 调用 WebAuthn API │
│ │───────────────>│ │
│ │ │ │
│ 5. 用户验证(指纹/Face ID) │ │
│<─────────────────────────────────│ │
│────────────────────────────────>│ │
│ │ │ │
│ │ 6. 返回公钥 + 签名 │
│ │<───────────────│ │
│ │ │ │
│ │ 7. 发送凭证到服务器 │
│ │────────────────────────────────>│
│ │ │ │
│ │ 8. 验证并存储公钥 │
│ │<────────────────────────────────│
│ │ │ │
前端 JavaScript 代码
// 注册 Passkey
async function registerPasskey() {
// 1. 从服务器获取注册选项
const optionsResponse = await fetch('/api/webauthn/register/options', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: 'alice' })
});
const options = await optionsResponse.json();
// 2. 转换为 WebAuthn 格式
options.challenge = base64ToArrayBuffer(options.challenge);
options.user.id = base64ToArrayBuffer(options.user.id);
// 3. 调用 WebAuthn API 创建凭证
const credential = await navigator.credentials.create({
publicKey: options
});
// 4. 发送凭证到服务器
const attestationResponse = {
id: credential.id,
rawId: arrayBufferToBase64(credential.rawId),
type: credential.type,
response: {
clientDataJSON: arrayBufferToBase64(
credential.response.clientDataJSON
),
attestationObject: arrayBufferToBase64(
credential.response.attestationObject
)
}
};
await fetch('/api/webauthn/register/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(attestationResponse)
});
}
// 登录
async function loginWithPasskey() {
// 1. 获取认证选项
const optionsResponse = await fetch('/api/webauthn/login/options', {
method: 'POST'
});
const options = await optionsResponse.json();
options.challenge = base64ToArrayBuffer(options.challenge);
options.allowCredentials = options.allowCredentials.map(cred => ({
...cred,
id: base64ToArrayBuffer(cred.id)
}));
// 2. 获取凭证
const assertion = await navigator.credentials.get({
publicKey: options
});
// 3. 发送给服务器验证
const assertionResponse = {
id: assertion.id,
rawId: arrayBufferToBase64(assertion.rawId),
type: assertion.type,
response: {
clientDataJSON: arrayBufferToBase64(
assertion.response.clientDataJSON
),
authenticatorData: arrayBufferToBase64(
assertion.response.authenticatorData
),
signature: arrayBufferToBase64(
assertion.response.signature
)
}
};
await fetch('/api/webauthn/login/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(assertionResponse)
});
}
Rust 后端实现
使用 webauthn-rs 库:
use webauthn_rs::prelude::*;
use axum::{Router, routing::post, Json, Extension};
use std::sync::Arc;
use tokio::sync::RwLock;
use std::collections::HashMap;
// 用户凭证存储(实际应用中应该用数据库)
type CredentialStore = Arc<RwLock<HashMap<String, Vec<Passkey>>>>;
// 注册会话存储
type RegistrationStore = Arc<RwLock<HashMap<String, PasskeyRegistration>>>;
// 认证会话存储
type AuthenticationStore = Arc<RwLock<HashMap<String, PasskeyAuthentication>>>;
struct AppState {
webauthn: Webauthn,
credentials: CredentialStore,
registrations: RegistrationStore,
authentications: AuthenticationStore,
}
// 注册步骤 1:生成选项
async fn register_start(
Extension(state): Extension<Arc<AppState>>,
Json(req): Json<RegisterRequest>,
) -> Json<CreationChallengeResponse> {
let user_id = Uuid::new_v4();
// 生成注册挑战
let (ccr, reg_state) = state
.webauthn
.start_passkey_registration(
user_id,
&req.username,
&req.username,
None, // 不排除已有凭证
)
.expect("Failed to start registration");
// 保存注册状态
state.registrations
.write()
.await
.insert(req.username.clone(), reg_state);
Json(ccr)
}
// 注册步骤 2:验证并存储
async fn register_finish(
Extension(state): Extension<Arc<AppState>>,
Json(req): Json<RegisterFinishRequest>,
) -> Json<RegisterResponse> {
// 获取注册状态
let reg_state = state.registrations
.write()
.await
.remove(&req.username)
.expect("Registration not found");
// 验证并获取 Passkey
let passkey = state
.webauthn
.finish_passkey_registration(&req.credential, ®_state)
.expect("Failed to finish registration");
// 存储凭证
state.credentials
.write()
.await
.entry(req.username.clone())
.or_default()
.push(passkey);
Json(RegisterResponse { success: true })
}
// 登录步骤 1:生成挑战
async fn login_start(
Extension(state): Extension<Arc<AppState>>,
Json(req): Json<LoginRequest>,
) -> Json<RequestChallengeResponse> {
// 获取用户的凭证
let credentials = state.credentials
.read()
.await
.get(&req.username)
.cloned()
.unwrap_or_default();
// 生成认证挑战
let (rcr, auth_state) = state
.webauthn
.start_passkey_authentication(&credentials)
.expect("Failed to start authentication");
// 保存认证状态(关联用户名)
state.authentications
.write()
.await
.insert(req.username.clone(), auth_state);
Json(rcr)
}
// 登录步骤 2:验证签名
async fn login_finish(
Extension(state): Extension<Arc<AppState>>,
Json(req): Json<LoginFinishRequest>,
) -> Json<LoginResponse> {
// 获取认证状态
let auth_state = state.authentications
.write()
.await
.remove(&req.username)
.expect("Authentication session not found");
// 验证签名
let auth_result = state
.webauthn
.finish_passkey_authentication(&req.credential, &auth_state)
.expect("Authentication failed");
// 更新凭证计数器(防止克隆攻击)
// update_credential_counter(&auth_result);
Json(LoginResponse {
success: true,
username: req.username,
})
}
Passkey 如何防止攻击
防钓鱼
传统密码:
用户可能在假网站输入密码
假网站获得密码后可以在真网站使用
Passkey:
认证器验证网站来源(origin)
假网站 phishing.com 发起的认证
签名会包含 phishing.com 作为 origin
真网站 bank.com 验证时会拒绝
因为 origin 不匹配!
技术实现:
clientDataJSON 包含:
{
"type": "webauthn.get",
"challenge": "...",
"origin": "https://bank.com", ← 认证器填入
"crossOrigin": false
}
服务器验证 origin 是否是自己的域名
防重放
每次认证,服务器发送随机 challenge
攻击者即使截获了签名,也无法重用:
- 下次 challenge 不同
- 签名是对 challenge 的签名
- 旧签名对新 challenge 无效
防数据库泄露
服务器存储的是公钥
公钥泄露的后果:
- 公钥本来就是公开的
- 攻击者无法用公钥登录
- 攻击者无法从公钥推导私钥
- 没有任何可以离线破解的东西
防中间人
HTTPS 保护传输通道
即使中间人看到了通信内容:
- 看到的是公钥和签名
- 无法获得私钥
- 无法伪造签名
Passkey 的用户体验
注册流程
传统注册:
1. 想一个密码
2. 想一个不太简单的密码
3. 重复输入密码
4. 记住这个密码(或者存到密码管理器)
Passkey 注册:
1. 点击"创建 Passkey"
2. 验证身份(Touch ID / Face ID)
3. 完成
登录流程
传统登录:
1. 输入用户名
2. 输入密码(还记得吗?)
3. 可能需要 2FA
4. 登录成功
Passkey 登录:
1. 点击"用 Passkey 登录"
2. 验证身份(Touch ID / Face ID)
3. 登录成功
跨设备使用
同步 Passkey(Apple/Google 生态):
- 自动同步到你的所有设备
- 新设备登录 iCloud/Google 后即可使用
- 无需手动迁移
跨平台登录(如用 iPhone 登录 Windows):
1. 网站显示二维码
2. 用手机扫码
3. 手机验证身份
4. 电脑上登录成功
实施建议
渐进式迁移
阶段 1:Passkey 作为可选
- 保留密码登录
- 允许用户添加 Passkey
- 积累用户反馈
阶段 2:推广 Passkey
- 登录时提示"添加 Passkey"
- 强调安全性和便利性
- 但不强制
阶段 3:Passkey 优先
- 默认显示 Passkey 登录
- 密码登录作为备选
- 新用户默认用 Passkey
阶段 4(可选):密码淘汰
- 只对高安全需求服务
- 需要完善的账号恢复机制
账号恢复
Passkey 最大的挑战:丢失设备怎么办?
方案 1:多设备注册
- 鼓励用户在多个设备注册 Passkey
- 一个设备丢失,其他设备仍可登录
方案 2:同步 Passkey
- 使用 iCloud Keychain 或 Google Password Manager
- 设备丢失不影响 Passkey
方案 3:备用恢复方式
- 保留备用邮箱/手机验证
- 恢复码(打印保存)
- 人工客服验证
方案 4:硬件密钥备份
- 注册一个 YubiKey 作为备份
- 安全存放
核心认知
Passkey 的本质:用公钥密码学替代"共享秘密",从根本上消除密码的安全风险。
为什么更安全:
- 私钥从不传输,无法被窃取
- 公钥可以公开,泄露无影响
- 每次挑战不同,无法重放
- 绑定域名,防止钓鱼
为什么更好用:
- 无需记忆密码
- 生物识别一键登录
- 跨设备同步
关键组件:
- WebAuthn:浏览器 API 标准
- CTAP:与认证器通信协议
- 认证器:Touch ID、Face ID、YubiKey 等
引出下一篇
我们已经学习了认证的演进历史:
- Session:服务器记住你
- JWT:无状态令牌
- OAuth 2.0:第三方授权
- OpenID Connect:标准化身份层
- Passkey:告别密码
但在实际系统中,你不会只用一种方案。
下一篇,我们来做一个实战练习:如何设计一个现代认证系统,把这些技术组合起来。
常见问题
Q:Passkey 会完全取代密码吗?
A:短期内不会,但长期是趋势。
Passkey 普及的障碍:
- 用户教育成本
- 旧设备不支持
- 部分场景仍需密码(如设备加密)
- 企业系统迁移周期长
预期时间线:
2023-2025:早期采用者
2025-2027:主流网站支持
2027-2030:成为主要登录方式
2030+:密码成为备选
Q:同步 Passkey 安全吗?
A:相对安全,但引入新的风险。
优点:
- 私钥在云端加密存储
- 用户不会因丢失设备而丢失 Passkey
风险:
- 依赖云账号安全(Apple ID / Google 账号)
- 云服务商有理论上的访问能力
对比:
- 同步 Passkey:安全性略低,但实用性高
- 设备绑定 Passkey:安全性最高,但丢设备就没了
建议:
- 普通用户:同步 Passkey 足够
- 高安全需求:设备绑定 + 硬件备份
Q:我应该继续使用 YubiKey 吗?
A:取决于你的安全需求。
YubiKey 的优势:
- 物理隔离,最高安全等级
- 不依赖云服务
- 不依赖设备操作系统
YubiKey 的劣势:
- 需要随身携带
- 可能丢失
- 价格不便宜
建议:
- 普通用户:平台 Passkey(Touch ID/Face ID)足够
- 开发者/管理员:考虑 YubiKey 作为备份
- 高管/关键账号:强烈建议 YubiKey
下一篇:实战:设计一个现代认证系统
本系列: