第十章:多因素认证(MFA)
“密码是安全的最薄弱环节,MFA 是加固它的最有效手段。”
mindmap
root((多因素认证))
认证三要素
你知道的
你拥有的
你是谁
密码安全
bcrypt
Argon2
scrypt
MFA 方式
TOTP
FIDO2/WebAuthn
Passkey
推送通知
高级
自适应认证
MFA 疲劳攻击
10.1 认证三要素
多因素认证基于三类不同的认证因素:
因素 |
类型 |
示例 |
优点 |
缺点 |
|---|---|---|---|---|
你知道的 |
知识因素 |
密码、PIN、安全问题 |
简单、成本低 |
可被猜测、钓鱼、泄露 |
你拥有的 |
持有因素 |
手机、硬件密钥、智能卡 |
物理持有难以远程窃取 |
可丢失、被盗 |
你是谁 |
生物因素 |
指纹、面部、虹膜 |
难以伪造 |
隐私问题、无法更换 |
MFA = 使用两种或以上不同类型的因素
单因素认证(不安全):
用户 ──▶ [密码] ──▶ 通过 ✅
双因素认证(2FA):
用户 ──▶ [密码] ──▶ [TOTP 验证码] ──▶ 通过 ✅
知识因素 持有因素
三因素认证:
用户 ──▶ [密码] ──▶ [硬件密钥] ──▶ [指纹] ──▶ 通过 ✅
知识因素 持有因素 生物因素
10.2 密码认证的问题
常见攻击方式
攻击方式 |
描述 |
防御 |
|---|---|---|
暴力破解 |
尝试所有可能的密码组合 |
账户锁定、速率限制 |
字典攻击 |
使用常见密码列表 |
密码复杂度要求 |
撞库攻击 |
用泄露的密码尝试其他网站 |
密码唯一性教育 |
钓鱼攻击 |
伪造登录页面骗取密码 |
FIDO2/WebAuthn |
彩虹表 |
预计算哈希值反查密码 |
加盐哈希 |
键盘记录 |
恶意软件记录键盘输入 |
硬件密钥 |
密码存储
永远不要明文存储密码! 使用专门的密码哈希函数:
# ❌ 错误:明文存储
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位数字 │
│ 验证 ◀──┼────────────────────│ 显示 │
│ │ │ │
└──────────┘ └──────────┘
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 自适应认证
根据风险等级动态调整认证强度:
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 疲劳攻击,使用号码匹配等防御措施