代码改变世界

99% 准确率识别爬虫,六层防御还是被刷量——反爬虫的真相

2026-07-09 08:22  轩辕公子  阅读(11)  评论(0)    收藏  举报

作者:技术从业16年,踩过坑、做过技术负责人、带过团队
本文不讲概念堆砌,讲一次从协议层到业务层的完整拆解——反爬虫到底在防什么?每一层怎么破?以及为什么说技术的尽头是经济学。


起因:一场"刷量"事故让我开始认真研究反作弊

去年有个项目,上线不到两周,运营跑来找我:"老大,数据不对,每天凌晨 3 点流量暴增,但转化是零。"

我打开监控一看,凌晨 3 点,UV 曲线像被注射了肾上腺素,直直往上冲。但所有转化指标——注册、下单、留资——全都是一条平线。

那一刻我心里咯噔一下。不是系统挂了,是被刷了

做技术十六年,写过爬虫、也做过反爬,但这次不一样——对方不是简单的脚本小子,而是有组织、有工具的批量刷量。普通的 IP 限流和验证码在他们面前形同虚设。

这件事逼着我从零开始,把反爬虫这条链路从头到尾拆了一遍。这篇文章就是那次"拆解"的完整记录。从 TLS 握手到浏览器指纹,从行为建模到社交图谱,每一层我都会讲清楚原理是什么、为什么有效、以及怎么绕过


一、协议层指纹:TLS 握手中的身份泄露

1.1 问题场景

很多人以为反爬就是看 User-Agent。UA 伪造太简单了,Chrome 120 的 UA 字符串随便复制,Python 一行代码就能改。

但 UA 只是 HTTP 层的伪装。在 HTTP 之前,TLS 握手阶段就已经暴露了你的真实身份。

第一次意识到这个问题,是我用 Python requests 发了一个请求,后端 WAF 直接返回了 JS Challenge 页面。我检查了 UA、Headers、Cookie,全都跟浏览器一模一样。为什么还是被识别了?

答案藏在 TLS 握手的 ClientHello 消息里。

1.2 JA3/JA3S 指纹原理

TLS 握手时,客户端发送的 ClientHello 包含以下字段:

ClientHello {
    ProtocolVersion,          // TLS 1.2 / 1.3
    Random,                   // 32 字节随机数
    CipherSuites[],           // 加密套件列表(有序)
    Extensions[],             // 扩展列表(有序)
    EllipticCurves[],         // 椭圆曲线列表
    EcPointFormats[]          // 点格式列表
}

JA3 的计算方式是将上述字段拼接后做 MD5:

import hashlib

def compute_ja3(tls_version, cipher_suites, extensions,
                elliptic_curves, ec_point_formats):
    """
    JA3 = MD5(
        TLS版本 + "," +
        加密套件ID列表(用"-"连接) + "," +
        扩展ID列表(用"-"连接) + "," +
        椭圆曲线列表(用"-"连接) + "," +
        点格式列表(用"-"连接)
    )
    """
    raw = f"{tls_version}," \
          f"{'-'.join(map(str, cipher_suites))}," \
          f"{'-'.join(map(str, extensions))}," \
          f"{'-'.join(map(str, elliptic_curves))}," \
          f"{'-'.join(map(str, ec_point_formats))}"
    return hashlib.md5(raw.encode()).hexdigest()

关键洞察:不同客户端的 JA3 指纹差异极大。

客户端 典型 JA3 前缀 核心差异点
Chrome 120 771,4865-4866... 支持 TLS 1.3,扩展含 0x0017(extended_master_secret)
Firefox 121 771,4865-4867... 加密套件顺序与 Chrome 不同
Python requests 769,4866-4867-4868... TLS 1.3 支持不全,扩展数量少
Go net/http 771,4865-4866... 缺少 postHandshake 扩展

这意味着:即使完美伪造了 HTTP 层的 User-Agent,TLS 层仍然会暴露真实客户端类型。

做爬虫的同学可能不信——"我明明用的是 requests,UA 设的是 Chrome,为什么服务器知道?"上面的表就是答案。你的加密套件列表、扩展列表、椭圆曲线顺序,跟真正的 Chrome 不一样。这些值不是你主动设置的,是 TLS 库内置的。

1.3 JA3 的局限与演进

JA3 的局限在于它只看 ClientHello,不看 ServerHello。JA3S 补充了服务端响应指纹,两者配对才能更准确识别。

更先进的指纹方案:

  • JARM:主动探测,向目标发送 10 个特殊 ClientHello,根据 ServerHello 响应组合生成指纹,主要用于识别 C2 服务器
  • CYU (Campbell University):对 TLS 1.3 的 Cookie/PSK 扩展做更细粒度的解析
  • Traffic Analysis:不依赖明文,直接分析加密流的包大小、时序模式——这是目前最"难防"的一类

1.4 HTTP/2 连接级指纹

HTTP/2 引入了更多可指纹化的维度,这一点很多人忽略了:

SETTINGS 帧:
  HEADER_TABLE_SIZE        → 不同客户端默认值不同
  ENABLE_PUSH              → 浏览器通常禁用
  MAX_CONCURRENT_STREAMS   → Chrome: 1000, Firefox: 100
  INITIAL_WINDOW_SIZE      → Chrome: 6291456, Firefox: 131072
  MAX_FRAME_SIZE           → Chrome: 16384

PRIORITY 帧:
  帧的发送时机、权重值、依赖关系
  → Chrome 在连接建立后会发送一系列初始 PRIORITY 帧
  → Python hyper/h2 不会发送

Akamai 的研究指出,仅凭 HTTP/2 SETTINGS 帧的参数组合,就能以 99%+ 的准确率 区分浏览器和自动化工具。

读到这的时候我倒吸一口凉气。做了这么多年,我自认为对 HTTP 协议已经很熟了,但 HTTP/2 的这些指纹维度,我之前确实没注意过。你防爬虫防到一定程度,拼的不是谁更聪明,而是谁对协议的理解更深。


二、运行时环境检测:浏览器内的军备竞赛

2.1 浏览器指纹的五层体系

协议层防住了,攻击者会升级到 Headless 浏览器。Selenium、Puppeteer、Playwright 这些工具启动的是真实浏览器内核,TLS 指纹跟真浏览器一模一样。

那怎么区分?在浏览器内部做指纹采集。

一个完整的浏览器指纹采集系统通常包含以下层级:

┌─────────────────────────────────────────┐
│             浏览器指纹体系                │
├─────────────────────────────────────────┤
│  L1: 显式属性层                          │
│  UA, platform, language, plugins,       │
│  hardwareConcurrency, deviceMemory      │
├─────────────────────────────────────────┤
│  L2: 渲染指纹层                          │
│  Canvas, WebGL, AudioContext,           │
│  font enumeration                       │
├─────────────────────────────────────────┤
│  L3: 传感器与硬件层                       │
│  陀螺仪/加速度计校准值,                   │
│  电池API, 蓝牙枚举                       │
├─────────────────────────────────────────┤
│  L4: 行为特征层                          │
│  鼠标轨迹, 滚动模式, 打字节奏,           │
│  触摸屏手势特征                          │
├─────────────────────────────────────────┤
│  L5: 时序与一致性层                       │
│  各API响应时间差异,                       │
│  时区/语言/地理一致性                     │
└─────────────────────────────────────────┘

L1 是最容易被伪造的,改个 UA 字符串谁都会。但 L2 到 L5,每一层都在增加伪造的难度和成本。

2.2 Canvas 指纹的深度解析

Canvas 指纹是我觉得最"优雅"的一类。它的原理非常巧妙——同一段绘图指令,在不同硬件/驱动/字体渲染引擎下会产生微小差异。

async function generateCanvasFingerprint() {
    const canvas = document.createElement('canvas');
    canvas.width = 280;
    canvas.height = 60;
    const ctx = canvas.getContext('2d');

    // 阶段1:文本渲染(受字体引擎、抗锯齿算法影响)
    ctx.textBaseline = 'top';
    ctx.font = '14px Arial';
    ctx.fillStyle = '#f60';
    ctx.fillRect(125, 1, 62, 20);
    ctx.fillStyle = '#069';
    ctx.fillText('C9r6sL8n2xQ1 🦊', 2, 15);  // 含emoji,增加差异度
    ctx.fillStyle = 'rgba(102, 204, 0, 0.7)';
    ctx.fillText('C9r6sL8n2xQ1 🦊', 4, 17);

    // 阶段2:路径绘制(受GPU浮点精度影响)
    ctx.globalCompositeOperation = 'multiply';
    ctx.beginPath();
    ctx.arc(50, 50, 50, 0, Math.PI * 2, true);
    ctx.closePath();
    ctx.fill();

    // 阶段3:渐变叠加(受颜色插值算法影响)
    const gradient = ctx.createLinearGradient(0, 0, 280, 60);
    gradient.addColorStop(0, 'rgba(255,0,0,0.3)');
    gradient.addColorStop(0.5, 'rgba(0,255,0,0.5)');
    gradient.addColorStop(1, 'rgba(0,0,255,0.7)');
    ctx.fillStyle = gradient;
    ctx.fillRect(0, 0, 280, 60);

    // 提取像素数据并哈希
    const imageData = ctx.getImageData(0, 0, 280, 60);
    const hash = await crypto.subtle.digest('SHA-256', imageData.data);
    return Array.from(new Uint8Array(hash))
        .map(b => b.toString(16).padStart(2, '0'))
        .join('');
}

为什么 Canvas 指纹有效? 四个底层差异叠加:

  • GPU 的浮点运算精度差异 → arc() 渲染结果不同
  • 字体渲染引擎差异(FreeType / DirectWrite / CoreText)→ 抗锯齿像素不同
  • 颜色管理差异 → ICC 配置文件不同导致颜色插值结果不同
  • 子像素渲染顺序(RGB vs BGR)→ 影响文本渲染的像素排列

这四个差异点,每一个单独看都很微小,但叠加在一起,产生的信息熵足够区分数十亿台设备。

2.3 AudioContext 指纹

async function generateAudioFingerprint() {
    const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    const oscillator = audioCtx.createOscillator();
    const analyser = audioCtx.createAnalyser();
    const gain = audioCtx.createGain();
    const scriptProcessor = audioCtx.createScriptProcessor(4096, 1, 1);

    // 构建音频处理图
    oscillator.type = 'triangle';
    oscillator.frequency.setValueAtTime(10000, audioCtx.currentTime);
    gain.gain.setValueAtTime(0, audioCtx.currentTime);

    oscillator.connect(analyser);
    analyser.connect(scriptProcessor);
    scriptProcessor.connect(gain);
    gain.connect(audioCtx.destination);

    // 采集处理后的音频样本
    return new Promise(resolve => {
        scriptProcessor.onaudioprocess = event => {
            const output = event.outputBuffer.getChannelData(0);
            const hash = computeHash(output);
            oscillator.stop();
            audioCtx.close();
            resolve(hash);
        };
        oscillator.start();
    });
}

原理:不同音频处理栈在浮点运算、采样率转换、滤波器实现上的差异,导致对相同输入信号产生不同的输出样本。这和 Canvas 指纹异曲同工——利用的是硬件和驱动的"非标准化"差异。

2.4 自动化工具检测:Selenium/Puppeteer/Playwright 的特征泄露

这是我实际工作中用得最多的一类检测。攻防双方在这个层面的博弈最激烈。

// ===== 检测点 1:WebDriver 属性 =====
// Selenium 默认设置 navigator.webdriver = true
if (navigator.webdriver) { /* 被检测 */ }

// ===== 检测点 2:CDP (Chrome DevTools Protocol) 痕迹 =====
const cdpIndicators = [
    () => window.cdc_adoQpoasnfa76pfcZLmcfl_Array,  // ChromeDriver 注入
    () => window.cdc_adoQpoasnfa76pfcZLmcfl_Promise,
    () => window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol,
    () => document.querySelector('[ng-version]'),      // Angular CLI 特征
];

// ===== 检测点 3:navigator 属性一致性 =====
const inconsistencies = [];
if (navigator.userAgent.includes('Windows') &&
    navigator.platform === 'MacIntel') {
    inconsistencies.push('UA/platform 不一致');
}
if (navigator.languages.length === 0) {
    inconsistencies.push('languages 为空');  // 无头浏览器常见
}

// ===== 检测点 4:函数 toString 检测 =====
const nativeToString = Function.prototype.toString;
function isNative(fn) {
    return nativeToString.call(fn).includes('[native code]');
}
// 如果 isNative(navigator.getBattery) === false,说明被 hook 了

// ===== 检测点 5:Error 堆栈分析 =====
try {
    null.undefined;  // 故意抛错
} catch (e) {
    const stack = e.stack;
    if (stack.includes('puppeteer') || stack.includes('selenium')) {
        // 被检测
    }
}

高级检测:Property Descriptor 分析

function detectPropertyTampering() {
    const suspicious = {};

    const navProps = ['webdriver', 'languages', 'platform', 'plugins'];
    for (const prop of navProps) {
        const descriptor = Object.getOwnPropertyDescriptor(
            Navigator.prototype, prop
        );
        if (!descriptor) {
            suspicious[prop] = 'descriptor 被删除';
        } else if (descriptor.get && !isNative(descriptor.get)) {
            suspicious[prop] = 'getter 被 hook';
        } else if (descriptor.value !== undefined &&
                   !isNative(descriptor.value)) {
            suspicious[prop] = 'value 被覆写';
        }
    }

    return suspicious;
}

这里有一个重要的认知:检测不是看"有没有异常",而是看"异常是否成体系"。 单独一个 navigator.webdriver = false 不代表什么,但同时出现 languages 为空、UA/platform 不一致、函数 toString 异常,这个组合就是强信号。


三、行为建模:从规则到统计的演进

3.1 规则引擎的局限

传统规则引擎长这样:

规则示例:
- 同一IP 10分钟内请求 > 50 → 拦截
- 请求间隔 < 200ms → 可疑
- User-Agent 为空 → 拦截
- 缺少 Referer 且为 POST → 可疑

规则引擎最大的问题是:可被逐步试探绕过。 攻击者可以慢慢调整参数,找到阈值边界。你设 50 次/10 分钟,他就调成 49 次;你设 200ms 间隔,他就调成 210ms。

纯规则防御,本质上是在跟攻击者玩"猜猜我设了多少"的游戏。这个游戏,攻击者永远赢——因为他有无限的试错机会。

3.2 统计异常检测

鼠标轨迹熵分析

import numpy as np
from scipy.stats import entropy

def compute_mouse_entropy(trajectory):
    """
    计算鼠标轨迹的熵值
    真人轨迹:高熵(随机性强)
    机器人轨迹:低熵(过于规则)
    """
    if len(trajectory) < 10:
        return 0

    points = np.array(trajectory)
    deltas = np.diff(points, axis=0)
    angles = np.arctan2(deltas[:, 1], deltas[:, 0])

    angle_changes = np.diff(angles)
    hist, _ = np.histogram(angle_changes, bins=36, density=True)
    hist = hist[hist > 0]

    return entropy(hist)


def compute_movement_smoothness(trajectory):
    """
    计算运动平滑度(加加速度/Jerk)
    真人:适中(有加速减速过程)
    直线脚本:极高(瞬间加速到匀速)
    贝塞尔曲线脚本:分布异常集中
    """
    points = np.array(trajectory, dtype=float)
    if len(points) < 5:
        return 0

    v = np.diff(points, axis=0)      # 一阶导 = 速度
    a = np.diff(v, axis=0)           # 二阶导 = 加速度
    j = np.diff(a, axis=0)           # 三阶导 = 加加速度

    jerk_mag = np.linalg.norm(j, axis=1)
    return np.std(jerk_mag) / (np.mean(jerk_mag) + 1e-8)

时间间隔分布分析

from collections import Counter
import math

def page_dwell_time_analysis(dwell_times):
    """
    分析页面停留时间分布
    真人:对数正态分布,长尾
    机器人:集中在某个固定值附近
    """
    if not dwell_times:
        return {'verdict': 'insufficient_data'}

    # Benford 法则检测
    first_digits = [int(str(int(t))[0]) for t in dwell_times if t > 0]
    digit_counts = Counter(first_digits)
    total = sum(digit_counts.values())

    benford_expected = {d: math.log10(1 + 1/d) for d in range(1, 10)}

    chi_square = sum(
        (digit_counts.get(d, 0) / total - benford_expected[d]) ** 2
        / benford_expected[d]
        for d in range(1, 10)
    )

    mean_dt = np.mean(dwell_times)
    std_dt = np.std(dwell_times)
    cv = std_dt / (mean_dt + 1e-8)

    return {
        'chi_square': chi_square,
        'coefficient_of_variation': cv,
        'verdict': 'human' if cv > 0.5 and chi_square < 15.51 else 'bot'
        # 15.51 = 卡方分布在 df=8, α=0.05 的临界值
    }

行为检测的核心思想:不是找"坏行为",而是定义"正常行为的统计分布"。 偏离这个分布的就是异常。这个方法比规则引擎更难绕过,因为攻击者需要模拟的不是某个阈值,而是一整个概率分布。

3.3 深度学习行为模型

输入特征序列:
[t=0: mouse_move(x,y), t=1: mouse_move(x,y), t=5: scroll(200),
 t=30: click(btn), t=31: key_press('a'), ...]

          ┌──────────────────────────┐
          │   Embedding Layer        │
          │  (事件类型 → 向量)        │
          └───────────┬──────────────┘
                      │
          ┌───────────▼──────────────┐
          │   Transformer Encoder     │
          │   (捕捉事件间长程依赖)     │
          │   - Self-Attention        │
          │   - Positional Encoding   │
          └───────────┬──────────────┘
                      │
          ┌───────────▼──────────────┐
          │   Temporal Conv          │
          │   (捕捉局部时间模式)       │
          └───────────┬──────────────┘
                      │
          ┌───────────▼──────────────┐
          │   Attention Pooling       │
          │   (加权聚合时间步)         │
          └───────────┬──────────────┘
                      │
          ┌───────────▼──────────────┐
          │   Classification Head     │
          │   human / bot / uncertain │
          └──────────────────────────┘

为什么 Transformer 适合行为建模?

  • Self-Attention 能捕捉非连续事件间的关联(如"点击按钮 5 秒后才滚动"这种延迟模式)
  • Positional Encoding 保留了事件时序信息
  • 对序列长度不敏感,可以处理几秒到几分钟的行为记录

四、微信反作弊体系的深度拆解

这是让我最"开眼"的部分。微信的反作弊体系,跟一般 Web 平台的维度完全不同——它有一个其他平台无法复制的武器:社交图谱

4.1 设备指纹:从静态到动态

微信的设备指纹不依赖单一标识符,而是构建了一个多维向量:

class WeChatDeviceFingerprint:
    """
    微信设备指纹向量(推测架构)
    每个维度都有独立的价值和权重
    """

    def compute(self, device_info):
        vector = {}

        # ===== 硬件层 =====
        vector['cpu_arch'] = device_info['cpu_abi']
        vector['screen'] = f"{device_info['width']}x{device_info['height']}@{device_info['dpi']}"
        vector['ram_gb'] = device_info['total_mem'] >> 30
        vector['storage_gb'] = device_info['total_storage'] >> 30
        vector['battery_capacity'] = device_info['battery_capacity']
        vector['sensor_set'] = hash(tuple(sorted(device_info['sensor_list'])))

        # ===== 系统层 =====
        vector['os_version'] = device_info['android_version']
        vector['security_patch'] = device_info['security_patch_level']
        vector['build_fingerprint'] = device_info['build_fingerprint']
        vector['install_time'] = device_info['first_install_time']

        # ===== 网络层 =====
        vector['wifi_bssid'] = device_info.get('wifi_bssid', '')
        vector['cell_tower'] = device_info.get('cell_tower_id', '')
        vector['vpn_detected'] = device_info.get('vpn_active', False)
        vector['proxy_detected'] = self._detect_proxy(device_info)

        # ===== ID 层(可变但有关联性)=====
        vector['android_id'] = device_info['android_id']
        vector['gaid'] = device_info.get('google_ad_id', '')
        vector['oaid'] = device_info.get('oaid', '')  # 国内替代GAID
        vector['imei_hash'] = self._hash(device_info.get('imei', ''))

        # ===== 行为层(动态更新)=====
        vector['app_launch_pattern'] = self._encode_launch_times(
            device_info['launch_timestamps_7d']
        )
        vector['typical_active_hours'] = self._encode_active_hours(
            device_info['active_hours_30d']
        )
        vector['geo_cluster'] = self._cluster_geo(
            device_info['location_history_7d']
        )

        return self._embed(vector)

    def _embed(self, vector):
        """将异构特征映射到统一的嵌入空间"""
        # 类似 Graph Neural Network 的思路
        # 设备 = 节点,共享属性 = 边
        # 同一物理设备的不同 ID 会在嵌入空间中距离很近
        pass

4.2 社交图谱验证:微信的核心壁垒

def compute_read_trust(read_event, social_graph):
    """
    计算单次阅读行为的可信度
    """
    score = 0.5  # 基准分

    # 因子1:读者与作者的关系强度
    relation = social_graph.get_relation(
        read_event.reader_id,
        read_event.author_id
    )
    if relation.is_subscriber:
        score += 0.15
    if relation.shared_group_count > 0:
        score += 0.10
    if relation.mutual_friends > 5:
        score += 0.05
    if relation.has_chat_history:
        score += 0.05

    # 因子2:读者的社交图谱健康度
    reader_graph = social_graph.get_graph(read_event.reader_id)
    if reader_graph.total_friends > 30:
        score += 0.05
    if reader_graph.interaction_diversity > 0.3:
        score += 0.05
    if reader_graph.account_age_days > 180:
        score += 0.05

    # 因子3:传播路径合理性
    if read_event.source == 'chat':
        score += 0.05  # 聊天转发,有明确传播链
    elif read_event.source == 'moments':
        score += 0.03  # 朋友圈,传播链稍弱
    elif read_event.source == 'direct_link':
        score -= 0.10  # 直接链接,无社交传播链,可疑
    elif read_event.source == 'search':
        score += 0.02  # 主动搜索,有一定可信度

    # 因子4:阅读行为本身的质量
    if read_event.dwell_time > 10:
        score += 0.05
    if read_event.scroll_depth > 0.5:
        score += 0.05

    return min(max(score, 0), 1.0)

社交图谱是微信反作弊的终极武器。你可以伪造设备指纹,可以模拟真人行为,但你没法伪造一个活生生的社交关系链——好友、群聊、朋友圈互动、聊天记录,这些数据是十几年积累下来的,不是注册几个新号就能构建的。

4.3 T+1 清洗系统

实时计数层(Redis)        →  显示的阅读量(含噪声)
         │
         ▼
Kafka 消息队列              →  所有阅读事件进入离线处理
         │
         ▼
Flink/Spark 批处理         →  T+1 离线分析
         │
         ├──→ 异常检测模型        →  标记可疑阅读事件
         ├──→ 设备指纹聚类        →  识别同设备多账号
         ├──→ 社交图谱分析        →  识别无关系链的阅读群
         ├──→ 行为序列分析        →  识别模板化行为
         │
         ▼
清洗结果                     →  剔除可疑事件
         │
         ▼
更新计数                     →  第二天显示的阅读量(已清洗)

这就是为什么刷量后经常出现"第二天阅读量掉"的现象——离线清洗系统在工作。实时显示的是"含噪声"的计数,T+1 才是真实数据。

4.4 微信反作弊的层级防御

┌────────────────────────────────────────────────────┐
│  L0: 请求拦截层                                      │
│  - IP 信誉/速率限制                                  │
│  - 已知代理/机房 IP 拦截                              │
│  → 挡住 70% 的低级刷量                               │
├────────────────────────────────────────────────────┤
│  L1: 设备指纹层                                      │
│  - 多维设备ID关联                                     │
│  - 设备-账号绑定关系                                  │
│  → 挡住 15% 的中级刷量                               │
├────────────────────────────────────────────────────┤
│  L2: 行为分析层                                      │
│  - 停留时长/滚动深度/交互事件                          │
│  - 访问时间分布                                       │
│  → 挡住 10% 的高级刷量                               │
├────────────────────────────────────────────────────┤
│  L3: 社交图谱层                                      │
│  - 读者与作者的关系链                                  │
│  - 传播路径合理性                                     │
│  - 读者社交图谱健康度                                  │
│  → 挡住 4% 的专业刷量                                │
├────────────────────────────────────────────────────┤
│  L4: 离线清洗层                                      │
│  - T+1 批量重算                                      │
│  - 跨文章/跨账号关联分析                               │
│  - 图算法识别刷量网络                                  │
│  → 清除剩余 1% 的漏网之鱼                             │
└────────────────────────────────────────────────────┘

五、CDN/WAF 的机器人防御架构

5.1 多层 Challenge 体系

以 Cloudflare 为例的 Challenge 流水线:

请求到达
    │
    ▼
[Phase 1: 被动检测] ─────────────────────┐
- IP 信誉库查询                            │
- JA3 指纹匹配                             │
- HTTP 头分析                              │
- 请求速率检查                             │
    │                                      │
    ├─ 信任分数 > 0.8 → 直接放行            │
    ├─ 信任分数 0.3~0.8 → 进入 Phase 2     │
    └─ 信任分数 < 0.3 → 进入 Phase 3       │
                                           │
[Phase 2: JS Challenge] ──────────────────┤
- 返回含 JS 计算题的页面                    │
- 验证浏览器环境真实性                      │
- PoW (Proof of Work) 计算                 │
    │                                      │
    ├─ 通过 → 设置 cf_clearance cookie     │
    └─ 失败 → 进入 Phase 3                 │
                                           │
[Phase 3: Interactive Challenge] ──────────┤
- Turnstile / hCaptcha / reCAPTCHA         │
- 需要真人交互                             │
    │                                      │
    ├─ 通过 → 放行                         │
    └─ 失败 → 封禁 IP                      │
                                           │
└──────────────────────────────────────────┘

5.2 Proof-of-Work Challenge 的实现

// Cloudflare JS Challenge 的核心逻辑(简化)
async function solveChallenge() {
    // 1. 浏览器环境指纹采集
    const envData = {
        screen: `${screen.width}x${screen.height}`,
        colorDepth: screen.colorDepth,
        plugins: Array.from(navigator.plugins).map(p => p.name),
        timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
        webgl: getWebGLFingerprint(),
        canvas: getCanvasFingerprint(),
        audio: await getAudioFingerprint(),
        fonts: await detectFonts(),
    };

    // 2. 计算 PoW
    const challenge = extractChallenge(document.body.innerHTML);
    const difficulty = challenge.difficulty;  // 通常 16-20 bit
    let nonce = 0;
    const target = BigInt('0x' + '0'.repeat(difficulty / 4) + 'f'.repeat(64 - difficulty / 4));

    while (true) {
        const hash = await sha256(challenge.seed + nonce.toString());
        if (BigInt('0x' + hash) < target) break;
        nonce++;
    }

    // 3. 提交结果
    const response = {
        env: envData,
        nonce: nonce,
        pow_hash: await sha256(challenge.seed + nonce.toString()),
        solve_time_ms: performance.now() - startTime,
    };

    // 4. 获取 clearance cookie
    await fetch('/cdn-cgi/l/chk_jsfl', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(response),
    });
}

PoW 的关键设计

  • 浏览器需要消耗真实 CPU 时间 → 无法用简单脚本绕过
  • solve_time_ms 过短 = 可能用了 GPU 加速 = 可疑
  • solve_time_ms 过长 = 可能是人工介入 = 通常放行
  • 环境指纹与 PoW 结果绑定 → 无法复用答案

5.3 滑动窗口限流算法

import time
from collections import defaultdict

class SlidingWindowRateLimiter:
    """
    精确滑动窗口限流器
    比固定窗口更平滑,避免窗口边界的突发
    """
    def __init__(self, max_requests, window_seconds):
        self.max_requests = max_requests
        self.window_seconds = window_seconds
        self.requests = defaultdict(list)

    def is_allowed(self, key):
        now = time.time()
        cutoff = now - self.window_seconds

        self.requests[key] = [
            t for t in self.requests[key] if t > cutoff
        ]

        if len(self.requests[key]) >= self.max_requests:
            return False

        self.requests[key].append(now)
        return True


class TokenBucketLimiter:
    """
    令牌桶限流器
    允许短时突发,但平均速率受控
    """
    def __init__(self, rate, burst):
        self.rate = rate        # 每秒补充令牌数
        self.burst = burst      # 桶容量(最大突发量)
        self.tokens = burst
        self.last_refill = time.time()

    def consume(self, tokens=1):
        now = time.time()
        elapsed = now - self.last_refill

        self.tokens = min(
            self.burst,
            self.tokens + elapsed * self.rate
        )
        self.last_refill = now

        if self.tokens >= tokens:
            self.tokens -= tokens
            return True
        return False

5.4 指纹关联图谱

WAF 会构建 IP-指纹-行为的关联图谱,用于识别协同攻击:

class FingerprintCorrelationGraph:
    """
    指纹关联图谱
    检测同一攻击者控制的多源请求
    """
    def __init__(self):
        self.graph = {
            'ip_to_fingerprints': defaultdict(set),
            'fingerprint_to_ips': defaultdict(set),
            'ip_to_behavior_hash': defaultdict(set),
        }

    def add_observation(self, ip, ja3_hash, ua_hash, behavior_hash):
        fp = f"{ja3_hash}:{ua_hash}"
        self.graph['ip_to_fingerprints'][ip].add(fp)
        self.graph['fingerprint_to_ips'][fp].add(ip)
        self.graph['ip_to_behavior_hash'][ip].add(behavior_hash)

    def detect_coordination(self, time_window=300):
        """
        检测协同攻击模式:
        1. 多个 IP 共享同一指纹 → 同一工具批量控制
        2. 多个 IP 行为模式相同 → 脚本模板化
        3. IP 段集中 → 同一代理池
        """
        alerts = []

        # 检测1:指纹共享
        for fp, ips in self.graph['fingerprint_to_ips'].items():
            if len(ips) > 5:
                alerts.append({
                    'type': 'fingerprint_sharing',
                    'fingerprint': fp,
                    'affected_ips': len(ips),
                    'severity': 'high'
                })

        # 检测2:IP 指纹多样性异常
        for ip, fps in self.graph['ip_to_fingerprints'].items():
            if len(fps) > 3:
                alerts.append({
                    'type': 'fingerprint_rotation',
                    'ip': ip,
                    'fingerprint_count': len(fps),
                    'severity': 'medium'
                })

        # 检测3:ASN 聚集度
        asn_counts = defaultdict(int)
        for ip in self.graph['ip_to_fingerprints']:
            asn = self._ip_to_asn(ip)
            asn_counts[asn] += 1
        for asn, count in asn_counts.items():
            if count > 10:
                alerts.append({
                    'type': 'asn_clustering',
                    'asn': asn,
                    'ip_count': count,
                    'severity': 'high'
                })

        return alerts

六、攻防演进趋势

6.1 攻击方的演进

代际 特征 反检测手段 局限性
Gen1 单线程脚本 伪造 UA 协议指纹暴露
Gen2 Headless 浏览器 完整浏览器环境 行为特征异常
Gen3 真实浏览器 + CDP 反检测插件 指纹仍可采集
Gen4 浏览器指纹注入 修改 Canvas/WebGL 返回值 时序一致性难保证
Gen5 真人众包 真实设备+真实行为 成本高、规模受限

6.2 防御方的演进

代际 特征 检测能力 局限性
Gen1 规则引擎 阈值拦截 容易绕过
Gen2 指纹采集 设备级识别 指纹可伪造
Gen3 行为建模 统计异常检测 误报率
Gen4 图谱关联 跨维度关联 计算成本高
Gen5 联邦学习+持续认证 实时自适应 工程复杂度

6.3 终极困境

一切技术检测的根本假设是:机器行为与人类行为存在统计可分性。

当攻击方使用真人众包(Gen5),这个假设就不再成立。此时防御方只能依赖:

  1. 经济成本博弈 — 让每次刷量的成本高于收益
  2. 数据一致性验证 — 刷量带来的流量无法带来对应的转化/互动
  3. 长期图谱演化 — 刷量网络的社交图谱是静态的,真实网络是动态生长的

复盘:四条铁律

那次刷量事故之后,我花了三个月把整个反作弊体系梳理了一遍。技术方案做了很多,但真正沉淀下来的,是几条认知:

  1. 分层防御,不要指望一层拦住所有。 协议指纹拦 70%,浏览器指纹拦 15%,行为分析拦 10%,图谱关联拦 4%,离线清洗兜底 1%。每一层都要有,但每一层都要接受"会被绕过"。

  2. 检测信号要"成体系"看,不要单点看。 一个特征异常可能是误报,三个特征同时异常就是强信号。指纹关联图谱的价值就在于此。

  3. 社交图谱是终极壁垒。 如果你做的是平台型产品,社交关系链是你最值钱的反作弊资产。设备指纹可以伪造,行为可以模拟,但活生生的社交关系没法批量构建。

  4. 反作弊的尽头是经济学。 你的目标不是让刷量不可能,而是让刷量不划算。当刷量成本 >= 真实获客成本时,刷量自然消亡。


本文仅作技术原理研究,不构成任何实施建议。