第十章:多因素认证(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 疲劳攻击,使用号码匹配等防御措施