mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
583 字
2 分钟
WebAuthn 与 FIDO2:无密码认证详解
2023-04-07

前言#

传统的密码认证面临钓鱼、撞库等严重安全威胁。FIDO2(Fast Identity Online 2)与 WebAuthn 标准让用户无需密码即可完成安全认证。本章详解无密码认证的原理、实现和应用。

一、为什么需要无密码#

1.1 密码的困境#

问题说明
密码疲劳用户需要记忆大量密码
弱密码用户倾向使用简单密码
钓鱼攻击伪造登录页面骗取密码
数据泄露密码库泄露影响所有账户

1.2 无密码的优势#

graph LR A["用户"] -->|"指纹/面容"| B["本地设备"] B -->|"公钥"| C["服务器"] C -->|"挑战"| B B -->|"签名响应"| C style A fill:#90EE90 style C fill:#FFB6C1
  • 防钓鱼:私钥从未离开设备
  • 防撞库:服务器只存公钥
  • 用户体验:指纹刷脸即可登录

二、FIDO2 架构#

2.1 组件关系#

graph TB A["客户端平台"] --> B["WebAuthn API"] B --> C["Authenticator"] D["Relying Party"] --> E["FIDO Server"] subgraph "客户端" B C end subgraph "服务端" D E end
组件说明
WebAuthn API浏览器原生认证 API
Authenticator认证器(手机、电脑、安全密钥)
Relying Party依赖方(应用服务端)
FIDO Server处理公钥凭证验证

三、核心概念#

3.1 密钥对架构#

# 注册时:生成密钥对
key_pair = {
"private_key": "设备本地保存,永不离开",
"public_key": "发送到服务器存储"
}
# 登录时:签名验证
challenge = "服务器下发的随机挑战"
signature = authenticator.sign(challenge, private_key)

3.2 认证器类型#

类型说明示例
平台认证器内置于设备的认证器手机指纹、电脑 Windows Hello
漫游认证器可移动的外部设备YubiKey、TOTP 设备

3.3 Passkey#

// Passkey 是 FIDO2 凭证的统称
// 在 iOS 上由 iCloud Keychain 同步
// 在 Android 上由 Google Password Manager 同步

四、注册流程#

4.1 注册序列图#

sequenceDiagram participant U as User participant B as Browser participant S as Server participant A as Authenticator S->>B:挑战 + 依赖方信息 B->>U: 调用 WebAuthn API U->>A: 验证用户(指纹/面容) A->>A: 生成密钥对 A->>B: 公钥 + 签名 B->>S: 认证响应 S->>S: 验证签名,存储公钥 Note over S: 注册完成

4.2 服务端实现#

class AuthService:
async def register(self, user_id, attestation_response):
# 1. 验证 attestation 签名
credential_public_key = verify_attestation(
attestation_response
)
# 2. 生成凭证 ID
credential_id = generate_credential_id()
# 3. 存储公钥和凭证
await db.credentials.create(
user_id=user_id,
credential_id=credential_id,
public_key=credential_public_key,
counter=0,
device_type="platform"
)

4.3 浏览器调用#

const credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array(challenge),
rp: {
name: "My Application",
id: "my-app.com",
},
user: {
id: new Uint8Array(userId),
name: "user@example.com",
displayName: "张三",
},
pubKeyCredParams: [
{ alg: -7, type: "public-key" }, // ES256
{ alg: -257, type: "public-key" }, // RS256
],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required",
},
},
});

五、认证流程#

5.1 认证序列图#

sequenceDiagram participant U as User participant B as Browser participant S as Server participant A as Authenticator S->>B: 挑战(随机数) B->>U: 调用 WebAuthn API U->>A: 验证用户(指纹/面容) A->>A: 用私钥签名挑战 A->>B: 签名响应 B->>S: assertion S->>S: 用公钥验证签名 Note over S: 登录成功

5.2 服务端验证#

async def authenticate(self, user_id, assertion):
# 1. 获取用户公钥
credential = await db.credentials.get(
user_id=user_id,
credential_id=assertion.credential_id
)
# 2. 构建验证数据
verify_data = build_verify_data(
assertion.challenge,
assertion.authenticator_data,
assertion.client_data
)
# 3. 验证签名
if not verify_signature(
credential.public_key,
verify_data,
assertion.signature
):
raise AuthError("Invalid signature")
# 4. 检查计数器(防止克隆)
if assertion.counter <= credential.counter:
raise AuthError("Potential cloned authenticator")
# 5. 更新计数器
await db.credentials.update_counter(
credential.id,
assertion.counter
)

六、WebAuthn API#

6.1 创建凭证#

// 注册新设备
async function register(userId, userName, displayName) {
const challenge = await fetch("/api/auth/challenge").then(r =>
r.arrayBuffer()
);
const credential = await navigator.credentials.create({
publicKey: {
challenge: new Uint8Array(challenge),
rp: { name: "My App", id: "my-app.com" },
user: {
id: new TextEncoder().encode(userId),
name: userName,
displayName: displayName,
},
pubKeyCredParams: [{ alg: -7, type: "public-key" }],
authenticatorSelection: {
authenticatorAttachment: "platform",
userVerification: "required",
},
},
});
await fetch("/api/auth/register", {
method: "POST",
body: credential,
});
}

6.2 获取凭证#

// 登录
async function login(userId) {
const challenge = await fetch("/api/auth/challenge").then(r =>
r.arrayBuffer()
);
const allowCredentials = await fetch("/api/auth/credentials").then(r =>
r.json()
);
const assertion = await navigator.credentials.get({
publicKey: {
challenge: new Uint8Array(challenge),
allowCredentials: allowCredentials.map(c => ({
type: "public-key",
id: new Uint8Array(c.id),
})),
userVerification: "required",
},
});
await fetch("/api/auth/login", {
method: "POST",
body: assertion,
});
}

七、安全特性#

7.1 抗钓鱼#

# rpId 验证防止钓鱼
# 如果用户在钓鱼网站调用 WebAuthn
# rpId 将是钓鱼网站的域名,无法通过验证
def verify_rp_id(rp_id, expected_rp_id):
assert rp_id == expected_rp_id, "RP ID mismatch"

7.2 设备认证#

# Authenticator 数据结构
auth_data = {
"rp_id_hash": "依赖方 ID 哈希",
"flags": {
"user_present": True, # 用户在场
"user_verified": True, # 用户已验证
"attested_credential": False,
"extension_data": False
},
"sign_count": 12345, # 签名计数器
"attested_credential_data": None
}

7.3 用户验证#

验证方式说明
指纹指纹传感器
面部识别3D 面部扫描
PIN设备 PIN 码
物理密钥安全密钥按钮确认

八、实际应用#

8.1 实现无密码登录#

flowchart LR A["登录页"] --> B{"已有 Passkey?"} B -->|是| C["一键登录"] B -->|否| D["注册 Passkey"] C --> E["指纹/面容"] D --> E E --> F["登录成功"]

8.2 多设备同步#

# Passkey 同步机制
# iOS: iCloud Keychain (端到端加密)
# Android: Google Password Manager
# Windows: Windows Hello + TPM
# 服务器无需关心同步细节
# 每个设备生成独立密钥对
# 用户可注册多个凭证

九、浏览器兼容性#

浏览器支持情况
Chrome 67+完整支持
Safari 14+完整支持
Firefox 60+完整支持
Edge 79+完整支持

十、总结#

WebAuthn 和 FIDO2 通过公钥密码学实现了真正的无密码认证。用户使用本地认证器(指纹、面容)解锁私钥,服务器只存储公钥。这种架构从根本上解决了钓鱼、撞库等密码安全问题,是认证领域的重大突破。

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

WebAuthn 与 FIDO2:无密码认证详解
https://blog.souloss.com/posts/web/webauthn-and-fido2/
作者
Souloss
发布于
2023-04-07
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时