# 第十章:多因素认证(MFA) > "密码是安全的最薄弱环节,MFA 是加固它的最有效手段。" ```{mermaid} mindmap root((多因素认证)) 认证三要素 你知道的 你拥有的 你是谁 密码安全 bcrypt Argon2 scrypt MFA 方式 TOTP FIDO2/WebAuthn Passkey 推送通知 高级 自适应认证 MFA 疲劳攻击 ``` ## 10.1 认证三要素 多因素认证基于三类不同的认证因素: | 因素 | 类型 | 示例 | 优点 | 缺点 | |------|------|------|------|------| | 你知道的 | 知识因素 | 密码、PIN、安全问题 | 简单、成本低 | 可被猜测、钓鱼、泄露 | | 你拥有的 | 持有因素 | 手机、硬件密钥、智能卡 | 物理持有难以远程窃取 | 可丢失、被盗 | | 你是谁 | 生物因素 | 指纹、面部、虹膜 | 难以伪造 | 隐私问题、无法更换 | **MFA = 使用两种或以上不同类型的因素** ``` 单因素认证(不安全): 用户 ──▶ [密码] ──▶ 通过 ✅ 双因素认证(2FA): 用户 ──▶ [密码] ──▶ [TOTP 验证码] ──▶ 通过 ✅ 知识因素 持有因素 三因素认证: 用户 ──▶ [密码] ──▶ [硬件密钥] ──▶ [指纹] ──▶ 通过 ✅ 知识因素 持有因素 生物因素 ``` ## 10.2 密码认证的问题 ### 常见攻击方式 | 攻击方式 | 描述 | 防御 | |---------|------|------| | 暴力破解 | 尝试所有可能的密码组合 | 账户锁定、速率限制 | | 字典攻击 | 使用常见密码列表 | 密码复杂度要求 | | 撞库攻击 | 用泄露的密码尝试其他网站 | 密码唯一性教育 | | 钓鱼攻击 | 伪造登录页面骗取密码 | FIDO2/WebAuthn | | 彩虹表 | 预计算哈希值反查密码 | 加盐哈希 | | 键盘记录 | 恶意软件记录键盘输入 | 硬件密钥 | ### 密码存储 **永远不要明文存储密码!** 使用专门的密码哈希函数: ```python # ❌ 错误:明文存储 password_db["user1"] = "my_password" # ❌ 错误:普通哈希(太快,容易暴力破解) import hashlib password_db["user1"] = hashlib.sha256(b"my_password").hexdigest() # ✅ 正确:使用 Argon2(现代首选) from argon2 import PasswordHasher ph = PasswordHasher( time_cost=3, # 迭代次数 memory_cost=65536, # 内存使用 64MB parallelism=4, # 并行线程数 ) # 存储 hashed = ph.hash("my_password") # $argon2id$v=19$m=65536,t=3,p=4$... # 验证 try: ph.verify(hashed, "my_password") print("密码正确 ✅") # 检查是否需要重新哈希(参数升级时) if ph.check_needs_rehash(hashed): new_hash = ph.hash("my_password") except Exception: print("密码错误 ❌") ``` ### 密码哈希函数对比 | 算法 | 年份 | 特点 | 推荐 | |------|------|------|------| | MD5/SHA | — | 通用哈希,太快 | ❌ 禁止 | | bcrypt | 1999 | 经典,CPU 密集 | ✅ 遗留系统 | | scrypt | 2009 | CPU + 内存密集 | ✅ 可用 | | Argon2id | 2015 | 竞赛冠军,最安全 | ✅ 首选 | ## 10.3 TOTP — 基于时间的一次性密码 TOTP(Time-based One-Time Password,RFC 6238)是最常用的 MFA 方式: ``` TOTP 工作原理: ┌──────────┐ ┌──────────┐ │ 服务端 │ │ 客户端 │ │ │ 1. 共享密钥 │ (手机App) │ │ secret ─┼───────────────────▶│ secret │ │ │ (注册时,通过二维码) │ │ │ │ │ │ │ 当前时间 │ │ 当前时间 │ │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ HMAC-SHA1(secret, time/30) │ HMAC-SHA1(secret, time/30) │ │ │ │ │ │ │ ▼ │ │ ▼ │ │ 6位数字 │ 2. 用户输入验证码 │ 6位数字 │ │ 验证 ◀──┼────────────────────│ 显示 │ │ │ │ │ └──────────┘ └──────────┘ ``` ```python import pyotp import qrcode # 1. 生成密钥 secret = pyotp.random_base32() print(f"密钥: {secret}") # 2. 生成二维码 URL(用于 Google Authenticator 等) totp = pyotp.TOTP(secret) uri = totp.provisioning_uri( name="walter@example.com", issuer_name="MyApp" ) print(f"URI: {uri}") # 生成二维码图片 qr = qrcode.make(uri) qr.save("totp_qr.png") # 3. 生成当前验证码 current_code = totp.now() print(f"当前验证码: {current_code}") # 4. 验证用户输入的验证码 user_input = "123456" is_valid = totp.verify(user_input, valid_window=1) # 允许前后 30 秒 print(f"验证结果: {is_valid}") ``` ## 10.4 FIDO2/WebAuthn — 无密码认证 FIDO2 是下一代认证标准,由 W3C WebAuthn 和 FIDO CTAP 组成: ``` FIDO2 架构: ┌─────────────────────────────────────────────┐ │ FIDO2 │ ├──────────────────┬──────────────────────────┤ │ WebAuthn │ CTAP │ │ (浏览器 API) │ (客户端到认证器协议) │ │ W3C 标准 │ FIDO Alliance 标准 │ └──────────────────┴──────────────────────────┘ ``` ### 注册流程 ``` ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ 用户 │ │ 浏览器 │ │ 服务器 │ │ │ │ │ │ (Relying Party)│ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │ │ │ │ 1. 请求注册 │ │ │───────────────▶│ │ │ 2. 返回挑战 │ │ │ (challenge + │ │ │ user info) │ │ │◀───────────────│ │ 3. 提示用户 │ │ │ 验证身份 │ │ │◀──────────────│ │ │ 4. 生物识别 │ │ │ 或 PIN │ │ │──────────────▶│ │ │ │ 5. 创建密钥对 │ │ │ 发送公钥+签名 │ │ │───────────────▶│ │ │ 6. 存储公钥 │ │ │ 注册成功 ✅ │ │ │◀───────────────│ ``` ### Passkey Passkey 是 FIDO2 的消费者友好版本: | 特性 | 传统密码 | TOTP | Passkey | |------|---------|------|---------| | 防钓鱼 | ❌ | ❌ | ✅ | | 防重放 | ❌ | ✅ | ✅ | | 用户体验 | 差 | 中等 | 优秀 | | 跨设备 | ✅ | 需同步 | ✅(云同步) | | 无密码 | ❌ | ❌ | ✅ | ## 10.5 MFA 疲劳攻击与防御 MFA 疲劳攻击(MFA Fatigue / MFA Bombing):攻击者反复触发 MFA 推送通知,直到用户烦躁地点击"批准"。 ``` 攻击流程: 1. 攻击者获取用户密码(通过钓鱼或泄露) 2. 反复尝试登录,触发 MFA 推送 3. 用户收到大量推送通知 4. 用户疲劳,误点"批准" → 攻击成功 防御措施: ┌─────────────────────────────────────────┐ │ ✅ 使用号码匹配(Number Matching) │ │ 用户必须输入屏幕上显示的数字 │ │ ✅ 限制推送频率 │ │ ✅ 显示登录上下文(IP、位置、设备) │ │ ✅ 使用 FIDO2 替代推送通知 │ │ ✅ 异常检测:短时间内多次 MFA 请求告警 │ └─────────────────────────────────────────┘ ``` ## 10.6 自适应认证 根据风险等级动态调整认证强度: ```python from dataclasses import dataclass from enum import Enum class RiskLevel(Enum): LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" @dataclass class LoginContext: ip_address: str device_id: str location: str timestamp: float user_agent: str def assess_risk(user_id: str, context: LoginContext) -> RiskLevel: """评估登录风险""" risk_score = 0.0 # 检查 IP 是否已知 if not is_known_ip(user_id, context.ip_address): risk_score += 0.3 # 检查设备是否已知 if not is_known_device(user_id, context.device_id): risk_score += 0.3 # 检查地理位置异常(不可能旅行) if is_impossible_travel(user_id, context.location, context.timestamp): risk_score += 0.4 # 检查是否在异常时间 if is_unusual_time(user_id, context.timestamp): risk_score += 0.1 if risk_score < 0.2: return RiskLevel.LOW elif risk_score < 0.5: return RiskLevel.MEDIUM elif risk_score < 0.8: return RiskLevel.HIGH else: return RiskLevel.CRITICAL def get_required_auth(risk: RiskLevel) -> list[str]: """根据风险等级决定认证要求""" requirements = { RiskLevel.LOW: ["password"], RiskLevel.MEDIUM: ["password", "totp"], RiskLevel.HIGH: ["password", "totp", "email_verification"], RiskLevel.CRITICAL: ["password", "fido2", "admin_approval"], } return requirements[risk] ``` ## 10.7 小结 - **MFA** 通过组合多种认证因素大幅提升安全性 - **Argon2id** 是密码哈希的现代首选 - **TOTP** 是最普及的 MFA 方式,但无法防钓鱼 - **FIDO2/Passkey** 是无密码认证的未来,天然防钓鱼 - **自适应认证** 根据风险动态调整认证强度,平衡安全与体验 - 警惕 **MFA 疲劳攻击**,使用号码匹配等防御措施