土法炼钢兴趣小组的算法知识备份

【身份与访问控制工程】风险感知认证:设备信任、异常登录与挑战升级

文章导航

分类入口
architecturesecurity

目录

传统的认证系统是”一次通过,终身信任”:用户输入密码、刷一次二次因子,然后在接下来的数小时甚至数天里,所有请求都被当作同一身份处理。这种模型在 2005 年的企业内网还算够用,但在今天,一条被钓鱼的密码、一台被植入木马的笔记本、一次 Session Cookie 被盗,就足以让攻击者在合法身份的掩护下横向移动数周。风险感知认证(Risk-Based Authentication,RBA)以及由此衍生的 Adaptive MFA,正是为了解决这个问题:它不再把认证当作一个二元决策(通过 / 失败),而是把每一次登录、每一次高危操作都视为一个带风险评分的概率事件,根据评分动态决定是否需要追加挑战(challenge),挑战到什么强度。本篇把这套机制拆开讲清楚——信号从哪里采、分怎么打、阈值怎么切、挑战怎么升,以及在生产里会踩到哪些坑。

风险评分与挑战升级流程

本系列的上一篇《MFA、TOTP、WebAuthn、Passkey 工程实践》介绍了各类二次因子本身的密码学与用户体验权衡;本篇讨论的是何时以何种强度触发这些因子,是上层的策略决策引擎。与此同时,RBA 是零信任架构里”持续验证(continuous verification)“原则在登录边界的具体落地,也是API 安全中动态访问控制的关键输入信号。

一、RBA 与 Adaptive MFA 的定位

1.1 NIST SP 800-63-3 的 AAL 分级

NIST SP 800-63-3 把认证强度分为三个 Authenticator Assurance Level(AAL):

RBA 的核心职责是:根据风险分数动态决定本次请求需要达到的 AAL。一个用户在公司办公室、用自己的 Mac、访问内部 wiki,AAL1 足够;同一个用户在陌生 IP 用新设备访问财务后台,就必须被拉升到 AAL2 甚至 AAL3。

1.2 RBA 与 Adaptive MFA 的关系

两个术语经常混用,但精细区分如下:

本文主要讨论 RBA 与 Adaptive MFA,continuous authentication 留到 workload identity 与零信任篇展开。

1.3 为什么值得花力气做

一个很直接的对比:只有密码时,Google 的研究显示泄露密码的账户被接管率约 0.01%/天;加上设备信任检查后下降约 10 倍;加上知识题挑战后再下降约 10 倍;加上 TOTP 或 Push 下降约 100 倍;加上 FIDO2 几乎清零。但 100% 对所有人强制 FIDO2 在企业里并不现实——用户体验劣化、硬件成本、灾备复杂度都会被无限放大。RBA 的价值在于把这 100 倍的收益只施加在需要的那 2%~5% 高风险流量上,剩下的流量继续走低摩擦路径。这是一条典型的 ROC 曲线优化问题:横轴误杀率,纵轴拦截率,目标是找到那个让业务 KPI 与安全 KPI 同时可接受的点。

二、信号源详解

RBA 的精度几乎完全取决于信号质量。下面把工业界常用的信号维度逐一拆开。

2.1 IP Reputation

IP 信誉是最便宜也最常被过度依赖的信号。核心子信号包括:

一个常见的工程实现是维护一张三态 IP 评分表

{
  "ip": "198.51.100.42",
  "asn": 16509,
  "asn_name": "AMAZON-02",
  "country": "US",
  "is_tor": false,
  "is_datacenter": true,
  "is_residential_proxy": false,
  "is_mobile_carrier": false,
  "blacklist_hits": ["spamhaus_drop"],
  "last_seen_good": "2026-03-10T12:00:00Z",
  "score": 72
}

刷新频率:TOR/黑名单每小时,ASN 归属每天,地理库(MaxMind GeoIP2)每周。

2.2 Geolocation 与 Impossible Travel

地理信号的核心是把 IP 转换成 country / city / lat-lng,然后与该账户最近一次成功登录位置比较。如果两次登录的时间差除以大圆距离大于”物理可达速度”(通常取 900 km/h,覆盖商业航班),就是 impossible travel,强信号触发挑战升级。

一个 Python 实现:

import math
from datetime import datetime, timezone

EARTH_RADIUS_KM = 6371.0
MAX_PLAUSIBLE_SPEED_KMH = 900.0  # 商业航班上限,留足裕量
GRACE_DISTANCE_KM = 50.0          # GeoIP 精度兜底,市内波动不算
MIN_TIME_DELTA_SECONDS = 60       # 一分钟内不判定,避免 NTP 抖动

def haversine_km(lat1, lon1, lat2, lon2):
    phi1, phi2 = math.radians(lat1), math.radians(lat2)
    dphi = math.radians(lat2 - lat1)
    dlambda = math.radians(lon2 - lon1)
    a = math.sin(dphi/2)**2 + math.cos(phi1)*math.cos(phi2)*math.sin(dlambda/2)**2
    return 2 * EARTH_RADIUS_KM * math.asin(math.sqrt(a))

def is_impossible_travel(prev_login, curr_login):
    """
    prev_login, curr_login 均为 dict:
      {"ts": datetime, "lat": float, "lon": float, "ip": str, "asn": int}
    """
    dt = (curr_login["ts"] - prev_login["ts"]).total_seconds()
    if dt < MIN_TIME_DELTA_SECONDS:
        return False, "time_delta_too_small"

    dist = haversine_km(prev_login["lat"], prev_login["lon"],
                         curr_login["lat"], curr_login["lon"])
    if dist < GRACE_DISTANCE_KM:
        return False, "within_city_noise"

    required_speed = dist / (dt / 3600.0)  # km/h
    if required_speed > MAX_PLAUSIBLE_SPEED_KMH:
        return True, f"need {required_speed:.0f} km/h for {dist:.0f} km in {dt/60:.1f} min"
    return False, f"speed {required_speed:.0f} km/h acceptable"

Go 版本大同小异,重点是同样三个参数:最大可达速度、市内抖动阈值、最小时间间隔。实际部署时还要:

  1. 排除同一 ASN 内部切换(同一运营商切 NAT 池很常见)。
  2. 排除已知 VPN 出口列表(用户主动开 VPN 导致 IP 跳国是正常现象,不应直接当作高风险,但应作为中等风险参与总分)。
  3. 用户授权的 GPS 定位(如果是移动 App)优先于 IP 定位。

2.3 Device Fingerprint

设备指纹是在浏览器或 App 侧采集一组能够稳定识别同一台设备的信号,哪怕用户清 Cookie、换网络也能继续追踪。常见维度:

一个浏览器侧采集片段(仅示意,生产环境建议用 FingerprintJS 或自研 worker):

async function collectFingerprint() {
  const canvas = document.createElement("canvas");
  canvas.width = 240; canvas.height = 60;
  const ctx = canvas.getContext("2d");
  ctx.textBaseline = "alphabetic";
  ctx.fillStyle = "#f60"; ctx.fillRect(125, 1, 62, 20);
  ctx.fillStyle = "#069"; ctx.font = "11pt Arial";
  ctx.fillText("Cw😀zltl-fp-v1", 2, 15);
  const canvasHash = await sha256(canvas.toDataURL());

  const gl = document.createElement("canvas").getContext("webgl");
  const dbg = gl.getExtension("WEBGL_debug_renderer_info");
  const gpu = dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : "unknown";

  return {
    canvas: canvasHash,
    gpu,
    ua: navigator.userAgent,
    lang: navigator.language,
    tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
    screen: `${screen.width}x${screen.height}x${screen.colorDepth}`,
    dpr: devicePixelRatio,
    platform: navigator.platform,
    hw_concurrency: navigator.hardwareConcurrency || 0,
    mem: navigator.deviceMemory || 0,
  };
}

采集到的原始字段通过 SimHash 或 LSH 得到一个稳定 ID,配合服务端记录的 JA3/JA4 和 Cookie 上的 deviceId 三路交叉比对:

2.4 行为生物特征

Behavior biometrics 是一个容易被厂商神化的领域,务实地讲:

现实中的一个经验:行为生物特征最适合作为 anti-bot 信号(区分人和自动化脚本),而不是 anti-impersonation 信号(区分 Alice 和 Bob)。对后者的准确率在公开数据集上大约在 80%~90%,离独立决策差得远,只能作为 3~10 分的加权项。

2.5 新设备 vs 已知设备

“新设备检测”是 RBA 最朴素也是最有效的信号之一。逻辑很简单:

  1. 用户首次在某设备上登录成功、并通过 MFA 挑战后,服务端签发一个 device binding token(通常是一个 HttpOnly + Secure + SameSite=Strict 的 Cookie,内容是一个对账户绑定的随机 128-bit ID + HMAC 签名,或者是一个存在 IndexedDB 里的 WebAuthn device-bound key)。
  2. 下次该设备登录时,客户端把 token 带上,服务端验证通过即认为是已知设备,分数大幅降低。
  3. token 自带 TTL(典型 30~90 天)和可撤销性(用户在”我的登录设备”页面能单独踢出某台设备)。

注意几个细节:

2.6 凭证信号:Credential Stuffing 与 HIBP

密码本身也是一个信号源。攻击者从其他站点拖库,拿到 email:password 对后批量对本站撞库(credential stuffing),这是账户接管的头号来源。相关信号:

示例:密码校验时异步调用 HIBP:

func checkBreached(password string) (int, error) {
    h := sha1.Sum([]byte(password))
    hex := strings.ToUpper(fmt.Sprintf("%x", h))
    prefix, suffix := hex[:5], hex[5:]
    resp, err := http.Get("https://api.pwnedpasswords.com/range/" + prefix)
    if err != nil { return 0, err }
    defer resp.Body.Close()
    body, _ := io.ReadAll(resp.Body)
    for _, line := range strings.Split(string(body), "\n") {
        parts := strings.SplitN(strings.TrimSpace(line), ":", 2)
        if len(parts) == 2 && parts[0] == suffix {
            count, _ := strconv.Atoi(parts[1])
            return count, nil
        }
    }
    return 0, nil
}

注意:HIBP 查询的是密码本身的泄露次数,对 credential stuffing 有参考价值;真正防撞库还需要速率限制 + 设备指纹聚合。

2.7 信号可信度与加权

不同信号的噪声水平差异巨大。工程中建议为每个信号标注三个元属性:

规则权重不是拍脑袋,而是 weight ∝ precision * log(1/base_rate)——越少见的信号一旦命中信息量越大。

三、风险评分模型架构

3.1 规则引擎 vs ML 模型

两种范式各有优劣,生产系统几乎都是混合的:

维度 规则引擎 ML 模型
延迟 P99 常在 1ms 量级 10~50ms,需考虑超时兜底
可解释性 天然可解释,每条规则都能指出触发原因 黑盒,需 SHAP 事后解释
维护成本 规则量大了容易互相冲突 需要持续标注、再训练、监控数据漂移
新攻击响应 改一条规则就能生效,分钟级 需重新训练或在线学习,天级
误报率 规则过粗时误报高 能捕捉复杂组合,误报通常更低
合规审计 容易通过审计 需要提供模型卡和可解释性报告

3.2 分层结构

实际落地建议分两层:

┌───────────────────────────────────────────────────────┐
│   第一层:Hard Rules(一票否决 / 一票放行)           │
│   - 账户在密码泄露库里命中    → +50                   │
│   - IP 在 TOR 出口表里        → +40                   │
│   - 设备绑定 token 有效       → -30                   │
│   - 来自 VIP 白名单 IP        → -20                   │
└───────────────────────────────────────────────────────┘
                      │
                      ▼
┌───────────────────────────────────────────────────────┐
│   第二层:ML Model(组合特征打分,0~100)             │
│   - 输入:上面的所有信号维度                          │
│   - 模型:GBDT(XGBoost/LightGBM)或 Isolation Forest │
│   - 输出:anomaly_score(相对该用户历史的异常程度)   │
└───────────────────────────────────────────────────────┘
                      │
                      ▼
          最终分数 = clamp(rule_score + ml_score, 0, 100)

这样做的好处是:Hard Rules 兜住可解释性与合规,ML 层负责拟合复杂组合,两者相加既快又稳。

3.3 规则引擎 DSL 示例

一个可序列化为 JSON 的规则格式(类似 Open Policy Agent / Cedar 的思路):

{
  "version": 3,
  "rules": [
    {
      "id": "rule.tor_exit",
      "desc": "来自 TOR 出口节点",
      "when": {"signal.ip.is_tor": true},
      "action": {"add_score": 40, "tag": "tor"}
    },
    {
      "id": "rule.impossible_travel",
      "desc": "不可能旅行",
      "when": {"signal.travel.impossible": true},
      "action": {"add_score": 35, "tag": "impossible_travel"}
    },
    {
      "id": "rule.new_device",
      "desc": "新设备",
      "when": {"signal.device.is_known": false},
      "action": {"add_score": 20, "tag": "new_device"}
    },
    {
      "id": "rule.known_device_discount",
      "desc": "已知设备折扣",
      "when": {
        "all": [
          {"signal.device.is_known": true},
          {"signal.device.bound_age_days": {"gte": 7}}
        ]
      },
      "action": {"add_score": -25, "tag": "trusted_device"}
    },
    {
      "id": "rule.breached_password",
      "desc": "密码命中 HIBP",
      "when": {"signal.creds.breach_count": {"gt": 0}},
      "action": {"add_score": 50, "tag": "breached_password", "force_ladder": "high"}
    },
    {
      "id": "rule.office_ip_whitelist",
      "desc": "办公网白名单",
      "when": {"signal.ip.cidr_in": ["203.0.113.0/24", "198.51.100.64/26"]},
      "action": {"add_score": -15, "tag": "office_ip"}
    },
    {
      "id": "rule.admin_role_floor",
      "desc": "管理员角色最低挑战 AAL2",
      "when": {"signal.user.roles": {"contains": "admin"}},
      "action": {"floor_ladder": "medium", "tag": "admin_floor"}
    }
  ]
}

要点:

3.4 ML 特征工程

常用的特征族(按 FE 成本从低到高排序):

  1. 上下文 one-hot:country_code、asn、os_family、browser_family。
  2. 速率类:该 IP 最近 1/5/60 分钟登录尝试数、该账户最近失败次数、该指纹对多少账户发起过登录。
  3. 历史匹配:当前国家与历史 top-3 国家的匹配度、设备与历史设备集合的 Jaccard 相似度。
  4. 时间类:登录时刻在用户习惯时段内的分位数、距上次登录的小时数的对数。
  5. 交叉特征:country × device_is_known、asn × user_role。

标签获取是最难的。几种常见做法:

生产模型上线后,关注三个指标:

四、Challenge Ladder 与阈值

4.1 分档与响应

典型的四档设计(数字是经验值,业务需按自己数据调):

分数 等级 响应 AAL 典型场景
0-30 Low 静默放行,记录日志 AAL1 同设备 + 同城 + 同时段
30-60 Medium TOTP 或邮件 OTP AAL2 新设备但同城、VPN 导致 IP 跳变
60-80 High Push + 显示登录地点 / Magic Link AAL2+ 不可能旅行、首次海外登录
80-100 Critical FIDO2 or 阻断 + 人工审核 AAL3 密码在泄露库 + 新设备 + TOR

“挑战升级”不一定只发生在登录时。实际系统会在敏感动作(改密码、改邮箱、绑新支付手段、批量下载、提额转账)时再重新评分一次,分高则 step-up。这对应 OIDC 协议层的 acr_values 参数。

把这件事真正做成系统,还需要把「分数」翻译成用户动作、服务端动作和审计动作:

分数段 用户看到什么 服务端做什么 审计侧要记什么
0-30 正常通过 正常建会话 信号快照、最终得分
30-60 轻量挑战 降低会话寿命、标记观察态 触发原因、挑战结果
60-80 强挑战或临时冻结敏感动作 只开放低风险页面,等待 step-up 成功 规则命中、设备指纹、地理差异
80-100 阻断、人工复核或 break-glass 流程 禁止建立高权限会话 全量决策轨迹、工单号、处置人

误报治理也必须跟着这张表一起设计。否则系统只会不断加规则,却没有能力回答「为什么这个用户被拦了」「这次拦截是否值得」。

4.2 OIDC ACR / AMR 与 Step-up

OIDC Core 第 2 节定义了两个与认证强度相关的 claim:

RP(Relying Party)发起 step-up 的 Authorization Request 示例:

GET /authorize?
    response_type=code
    &client_id=finance-portal
    &redirect_uri=https%3A%2F%2Ffinance.example.com%2Fcb
    &scope=openid%20payments
    &state=xyz
    &code_challenge=...
    &code_challenge_method=S256
    &acr_values=urn:example:aal2%20urn:example:aal3
    &max_age=300
    &prompt=login HTTP/1.1
Host: idp.example.com

关键参数解读:

IdP 成功后,ID Token 会回:

{
  "iss": "https://idp.example.com",
  "sub": "user-42",
  "aud": "finance-portal",
  "iat": 1745130000,
  "exp": 1745133600,
  "auth_time": 1745129940,
  "acr": "urn:example:aal3",
  "amr": ["pwd", "fido"],
  "sid": "sess-abc-123"
}

RP 的职责是显式校验 acr / amr

type IDTokenClaims struct {
    AuthTime int64    `json:"auth_time"`
    ACR      string   `json:"acr"`
    AMR      []string `json:"amr"`
    // ... 其他字段
}

func requireAAL3(claims *IDTokenClaims, maxAgeSec int64) error {
    if claims.ACR != "urn:example:aal3" {
        return errors.New("insufficient_acr: need aal3")
    }
    if time.Now().Unix()-claims.AuthTime > maxAgeSec {
        return errors.New("auth_too_old")
    }
    hasStrong := false
    for _, m := range claims.AMR {
        if m == "fido" || m == "hwk" {
            hasStrong = true
            break
        }
    }
    if !hasStrong {
        return errors.New("amr_missing_strong_factor")
    }
    return nil
}

常见错误:RP 只校验了 JWT 签名和 exp,没校验 ACR,于是攻击者用 AAL1 登录的 Token 也能调 AAL3 接口。这类问题在审计里经常被揪出来。

4.3 RFC 9470 OAuth Step-up Challenge

2023 年发布的 RFC 9470 定义了一种标准化的 step-up 机制:资源服务器在发现访问令牌不满足所需强度时,返回 401 + WWW-Authenticate: Bearer error="insufficient_user_authentication",客户端据此发起带 acr_values 的重新授权。示例响应头:

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="finance",
    error="insufficient_user_authentication",
    error_description="AAL3 required for wire transfer",
    acr_values="urn:example:aal3",
    max_age="300"

客户端 SDK 捕获后发起新一轮 PAR → Authorization → Code → Token,成功后 retry 原请求。比旧的”RP 预判需要 AAL3 再走一次 flow”优雅得多。

4.4 阈值校准

阈值不是拍脑袋定的,而是从历史数据拟合。常用方法:

  1. 用近 90 天的登录日志构造评测集,正样本是确认的 ATO / 失败后被用户申诉,负样本是随机成功登录。
  2. 画 ROC 曲线:横轴 FPR(误报率),纵轴 TPR(召回率)。
  3. 根据业务选点:C 端通常接受 FPR<=2% 换 TPR>=75%;金融后台接受 FPR<=10% 换 TPR>=95%。
  4. 把选定点的评分作为 medium/high 档的阈值。

阈值上线后,每周跑一次再校准,数据漂移严重时自动抬高 Push 档阈值,避免大面积误拦。

4.5 Challenge 类型的优劣对比

档位定了还不够,每档具体选什么挑战类型也有讲究:

挑战类型 防钓鱼 防 SIM 交换 用户摩擦 成本 说明
SMS OTP 短信费 NIST 已不推荐作为主因子
Email OTP 邮箱被控则整条链失守
TOTP 弱(可被中转) 经典方案
Push + number matching APNS/FCM 必须加数字匹配抗 MFA 疲劳
FIDO2 / Passkey 低(用户熟悉后) 硬件或内置 AAL3 首选
Magic Link 邮箱被接管即失守
生物识别(本地) 极低 内置 作为 FIDO2 的 UV,不独立使用
知识题(安全问题) 极弱 几乎所有答案都能社工,不推荐

一个经验法则:低风险用熟悉的(TOTP/Push),高风险用强的(FIDO2),从不使用安全问题

4.6 一个完整的决策伪代码

def decide_challenge(ctx):
    score, tags = rule_engine.evaluate(ctx)
    if not any(t == "force_ladder_critical" for t in tags):
        score += ml_model.predict(ctx.features)
    score = max(0, min(100, score))

    ladder = "low"
    if score >= 30:  ladder = "medium"
    if score >= 60:  ladder = "high"
    if score >= 80:  ladder = "critical"

    # 角色 floor
    if "admin" in ctx.user.roles and ladder == "low":
        ladder = "medium"
    # 敏感操作 floor
    if ctx.operation in SENSITIVE_OPS and ladder in ("low", "medium"):
        ladder = "high"

    challenge = {
        "low":      {"aal": "aal1", "methods": []},
        "medium":   {"aal": "aal2", "methods": ["totp", "push_num"]},
        "high":     {"aal": "aal2", "methods": ["push_num", "magic_link"]},
        "critical": {"aal": "aal3", "methods": ["fido2"]},
    }[ladder]

    audit_log(ctx, score, tags, ladder, challenge)
    return challenge

五、设备信任与 Posture

5.1 Device Binding

最简单的 device binding 是签发一张设备身份令牌,本质是给设备分配一对密钥对,私钥存在设备可信存储里(iOS Secure Enclave、Android StrongBox、Windows TPM、macOS Keychain with LocalAuthentication),每次登录时设备用私钥签一个 nonce 证明自己持有。这正是 WebAuthn 的工作模式,所以在前端用 navigator.credentials.create({ publicKey: ... }) 创建一个 device-bound passkey(residentKey=discouraged, userVerification=preferred)即可同时解决 device binding 和 MFA 两个问题。

如果不能用 WebAuthn(老浏览器、自研 App),可以自己实现:

  1. 首次 enrollment:设备生成 EC P-256 密钥对,私钥存 Keychain;公钥 + 设备元信息 POST 到后端。
  2. 后端签发一个 device_id(UUID),与账户、公钥绑定入库。
  3. 下次登录:客户端发起 login,服务端返回 nonce,客户端用私钥签 nonce 回传,服务端验签。
  4. 撤销:用户在”我的设备”页面点”移除”,后端删除该 device_id 的绑定记录。

5.2 Device Posture

企业场景下,仅仅”这台设备是已知的”还不够,还要证明它处于合规状态。常检查的 Posture 信号:

采集方式有两条路:

一旦 Posture 失败(例如 FileVault 被关),策略可以是:

5.3 设备证书:SCEP 与 ACME for Device

大型企业会给每台受管设备签发一张 x509 证书,登录时走 mTLS,IdP 验证客户端证书即完成设备身份。两种常用签发协议:

证书私钥必须落在硬件可信存储里,不能 export。这样即使攻击者钓走用户密码也拿不到设备证书,从而无法完成 mTLS 握手。

六、工程坑点

6.1 VPN 导致的 impossible travel 大规模误报

典型场景:用户在北京办公室用公司 VPN(出口在香港),然后手机切到 4G 直连,IP 从香港跳到北京。短短 1 分钟内国家从 HK 变 CN,impossible travel 规则触发,全公司都被 TOTP 轰炸。

缓解办法:

  1. 维护公司出口 IP 白名单(包括所有 VPN 集群、办公网 NAT),命中白名单不参与 impossible travel 计算。
  2. 维护已知 VPN / 云服务 ASN 列表,跨 VPN 跳 IP 只加中等分(10~15)而不是高分(35+)。
  3. 用 Client-side 上报的真实 Wi-Fi BSSID / 系统时区替代 IP 地理定位,这两者比 IP 更稳定。

6.2 办公网统一出口 IP 命中 IP Reputation

公司有 5000 人,全都走同一个 NAT 出口,一旦某一个人电脑中毒对外扫描,这个 IP 就会被公开情报打上”恶意”标签,导致全公司被全球网站拦截。

缓解:

  1. 企业出口 IP 做分用户 NAT 池,至少按部门分。
  2. 向黑名单机构主动申报并监控,国内如 NSFOCUS、国外如 Spamhaus Removal。
  3. 内部 RBA 对本公司出口 IP 直接加白名单,不走公开情报。

6.3 跨时区团队的”异常时段”误报

“凌晨 3 点登录”在亚洲团队看是怪,在欧洲同事看是正常工作时间。RBA 模型如果只按服务器本地时间做特征,跨时区团队全部变成高风险。

缓解:以用户设备时区用户常驻国家为基准计算”本地小时”,而不是用服务器 UTC。

6.4 CAPTCHA / MFA 疲劳攻击

高风险评分触发密集 Push 通知,攻击者可以用这个”消耗”用户——反复尝试登录,直到用户在 100 条通知里随便点一下 Approve。2022 Uber 事件就是这么被攻陷的。

缓解:

  1. Push 通知加数字匹配(Microsoft Authenticator 的 number matching):用户必须输入登录页上显示的 2 位数字才算通过。
  2. 同一账户 60 秒内超过 3 次 Push 自动降级为 TOTP + 要求审核。
  3. Anomaly 检测到 push bombing 模式立即锁账户。

6.5 冷启动数据不足

新用户没有历史登录记录,ML 模型没法判断”这是不是他本人的习惯设备/地点”。如果直接按新用户规则强制 MFA,注册转化率会掉 10~20%。

缓解:

  1. 新账户 7 天内用弱规则(只看 IP reputation 与设备指纹黑名单),不做行为对比。
  2. 首次登录后主动引导”信任此设备”按钮,让用户显式 enrollment。
  3. 利用群体特征:和同一注册渠道 / 同一地域的新用户群体对比。

6.6 Device Fingerprint 的稳定性问题

Safari 17 之后默认启用 Privacy-preserving features,canvas 加噪声、UA 被 freeze;iOS Safari 的指纹会在每个 tab 不同。Chrome 也在逐步推进 Privacy Sandbox。不要把 fingerprint 当长期稳定 ID,它的 TTL 只有 7~30 天,仅作为短期辅助信号。

6.7 ML 模型标签噪声

“用户事后说不是我登录的”里有 30% 实际上是用户自己忘了,另外还有用户主动欺诈的场景(否认自己的操作以免责)。训练数据如果直接用工单标签会有大量噪声。

缓解:标签需要二次核实(如结合设备日志),或者用 PU Learning(Positive-Unlabeled)方法处理高噪声正样本。

6.8 Session 固定 vs 动态降级

一个不小心容易犯的设计错误:Session 建立时评分为 25(低风险),发一个 24 小时 Cookie;但用户跑去咖啡厅连公共 Wi-Fi,IP 变了、网络变了,Session 还在。此时应当动态重新评分并决定是否让这张 Session 降级(要求重新 MFA 才能做敏感操作)而不是无脑信任它。

做法:每次敏感 API 调用前,查询当时的实时风险分数(带 TTL 60 秒的缓存),分高就 WWW-Authenticate 401 触发 step-up。

6.9 用户隐私与合规边界

采集 canvas/WebGL 指纹、鼠标轨迹、键盘节奏,在欧盟 GDPR 和加州 CCPA 下都属于个人数据处理。合规问题常见的几个:

6.10 误报的业务代价

不要只看模型的 AUC。一次误杀的代价:

因此在评估策略时应该把”每万次登录的业务损失估值”做进指标:business_cost = false_positive_rate * avg_conversion_value * DAU。策略调整前做 A/B 测试,跟踪转化率、工单量、投诉率三条曲线。

七、落地建议

  1. 先有日志,再谈评分。很多团队一上来就想买一个 RBA 产品,但连”过去 30 天每个账户的登录 IP / UA / 时间 / 结果”这张基础表都没全。RBA 的第一步是把登录事件标准化落库(字段至少 12 个:ts、user_id、ip、asn、country、city、ua、device_id、ja3、result、mfa_type、session_id),再谈模型。
  2. 从 Hard Rules 开始,ML 是锦上添花。10~20 条基于 IP reputation、impossible travel、新设备、泄露密码的规则,覆盖 80% 的攻击场景。模型能多拿 10~15 个百分点的召回,但需要 3 个月以上的数据积累。
  3. 挑战升级务必是可逆的。高分场景下弹 FIDO2,但一定要给降级通道:用户丢了 key,还能通过”客服视频验证 + 24 小时冷静期”恢复,否则会制造大量账户锁死工单。
  4. 给信任设备一个明确入口。用户登录后显示”这是你常用设备吗?信任 30 天”的勾选框,把主动权交给用户——这比算法猜”这是不是他常用设备”准确得多。
  5. 把评分过程写进审计日志。每次挑战决策至少记录:总分、触发的规则 ID 列表、被挑战的等级、挑战结果、用户的 UA/IP。合规审计和事后复盘都离不开它。
  6. 保留 emergency kill switch。模型或规则上线后发生大规模误杀时,能一键回滚到”只做黑名单 + 密码”的最低档。这个按钮必须是 runbook 级别的。
  7. 与 SOC 团队共享信号。登录时的 IP reputation、device posture 信号对 SIEM 有高价值;反过来 SOC 发现的威胁指标也应当推回 RBA。双向闭环。
  8. 隐私合规。Canvas/WebGL fingerprint、鼠标轨迹在 GDPR 下属于个人数据,隐私政策里必须声明采集目的为”防止账户接管”,保留期限要明确(建议不超过 90 天)。国内的《个人信息保护法》同样要求明示并获取同意,不要把这些信号当作”技术细节”隐藏。
  9. UX 与安全的 ROC 平衡。指标上同时追踪”挑战通过率”(安全 KPI)与”挑战放弃率”(UX KPI)。每次策略变更都要评估两者的移动方向,不能单向优化。
  10. 用真实红蓝对抗迭代。每季度红队用合法采集的凭证 + 云 IP + 新设备跑一次完整的 ATO 演练,看 RBA 能不能拦住。攻防思维驱动策略迭代比单纯看监控指标更有效。

八、参考资料

  1. NIST SP 800-63-3(Digital Identity Guidelines)与 800-63B(Authentication and Lifecycle Management)
  2. OpenID Connect Core 1.0,第 2 节 ID Token(ACR/AMR/auth_time)
  3. RFC 8176,Authentication Method Reference Values
  4. RFC 9470,OAuth 2.0 Step Up Authentication Challenge Protocol
  5. RFC 8894,Simple Certificate Enrollment Protocol (SCEP)
  6. W3C Web Authentication Level 3(WebAuthn)
  7. FIDO Alliance,Device Attestation 与 Enterprise Attestation 白皮书
  8. Google Research,“New Research: Lessons from Password Checkup in Action”(2019)
  9. Microsoft,“Defending against Multi-Factor Authentication (MFA) Fatigue Attacks”(2022)
  10. Cloudflare,“Introducing the JA4 TLS fingerprint”(2023)
  11. MaxMind GeoIP2 Accuracy Report(每季度更新)
  12. Spamhaus DROP / EDROP 列表使用指南
  13. OASIS Cedar Policy Language(可作为规则 DSL 参考)
  14. OWASP Authentication Cheat Sheet 与 Session Management Cheat Sheet
  15. Apple Platform Security Guide,Device Attestation 章节
  16. Android Keystore System 文档,Key Attestation 章节
  17. CISA,“Implementing Phishing-Resistant MFA”(2022)
  18. 本站:MFA、TOTP、WebAuthn、Passkey 工程实践
  19. 本站:零信任架构与持续验证
  20. 本站:API 安全中的动态访问控制

上一篇MFA、TOTP、WebAuthn、Passkey 工程实践

下一篇服务身份:mTLS、SPIFFE/SPIRE 与 Workload Identity

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-04-21 · architecture / security

【身份与访问控制工程】IAM 全景:为什么这是高价值赛道

身份与访问控制从一个登录框演进为横跨合规、运维、平台工程和安全的系统工程。本文从一家 SaaS 公司被大客户卡在 SOC 2 合规的真实触发器切入,拆解 IAM、CIAM、IGA、PAM、SSO、目录服务六个子领域的边界,分析 Okta、Entra ID、Auth0、Keycloak、Ping 等主流厂商的定位与落差,给出工程师视角的介入判据与选型路径。

2026-04-21 · architecture / security

【身份与访问控制工程】企业单点登录:OIDC 与现代 SSO

B2B 大客户的安全团队只认自家 Okta,集团五条产品线要统一账号,合规团队要求所有入口都可审计——这些需求最终都落在同一个协议上:OpenID Connect。本文从一个真实的商业谈判场景切入,系统拆解 OIDC 在 OAuth 2.0 之上增加了什么、授权码流程的每一个字段为什么存在、企业集成里最常见的六类实现坑,以及 Discovery、JWKS、Dynamic Client Registration、mTLS Client Auth 的工程细节,帮助你把 SSO 从 demo 做到可以放进 SOC 2 审计报告里。


By .