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),这个假设就不再成立。此时防御方只能依赖:
- 经济成本博弈 — 让每次刷量的成本高于收益
- 数据一致性验证 — 刷量带来的流量无法带来对应的转化/互动
- 长期图谱演化 — 刷量网络的社交图谱是静态的,真实网络是动态生长的
复盘:四条铁律
那次刷量事故之后,我花了三个月把整个反作弊体系梳理了一遍。技术方案做了很多,但真正沉淀下来的,是几条认知:
-
分层防御,不要指望一层拦住所有。 协议指纹拦 70%,浏览器指纹拦 15%,行为分析拦 10%,图谱关联拦 4%,离线清洗兜底 1%。每一层都要有,但每一层都要接受"会被绕过"。
-
检测信号要"成体系"看,不要单点看。 一个特征异常可能是误报,三个特征同时异常就是强信号。指纹关联图谱的价值就在于此。
-
社交图谱是终极壁垒。 如果你做的是平台型产品,社交关系链是你最值钱的反作弊资产。设备指纹可以伪造,行为可以模拟,但活生生的社交关系没法批量构建。
-
反作弊的尽头是经济学。 你的目标不是让刷量不可能,而是让刷量不划算。当刷量成本 >= 真实获客成本时,刷量自然消亡。
本文仅作技术原理研究,不构成任何实施建议。
浙公网安备 33010602011771号