583 字
2 分钟
WebAuthn 与 FIDO2:无密码认证详解
前言
传统的密码认证面临钓鱼、撞库等严重安全威胁。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/ 部分信息可能已经过时
相关文章 智能推荐
1
WebRTC:实时通信协议详解
网络 深入解析 WebRTC 协议的架构、工作原理和应用场景
2
gRPC:高性能 RPC 框架详解
网络 深入解析 gRPC 协议的工作原理、Protocol Buffers 和流式调用
3
容器网络
容器运行时 容器网络的核心问题是——隔离的 Network Namespace 如何与外部通信?详细解读 veth pair(虚拟网卡对)、bridge(虚拟网桥)、iptables/NAT(地址转换)、CNI(容器网络接口)的完整链路,以及 Docker 的四种网络模式和 Kubernetes 的 Pod 网络模型——从「容器能 ping 通外网」到「理解每一条网络规则」。
4
OAuth 2.0 与 OIDC 协议:授权与身份认证
网络 深度解读 OAuth 2.0 授权码流程、JWT 令牌、刷新令牌、OIDC 单点登录
5
DNS 协议详解:域名系统的分层架构与缓存策略
Web 技术深入 深度解读 DNS 协议——从递归查询与迭代查询的完整流程,到 DNSSEC 签名验证链与 DoH/DoT 加密解析,全面理解域名系统的设计






