不弹验证码也能识别机器人?行为风控与会话评分原理
不弹验证码也能识别机器人?行为风控与会话评分原理
摘要:成熟的 Bot Management 不会只检查某一次请求,而会把请求频率、时间间隔、页面路径、客户端一致性和挑战结果聚合为会话风险分。本文用一个可运行的 Python 演示评分器讲清“信号—特征—评分—处置”的完整链路,并重点讨论误报、隐私和无障碍边界。
适合谁
本文适合:
- 只做过 IP 限速、验证码,想进一步理解行为风控的后端开发者;
- 在自有网站中治理采集、撞库、抢购或接口滥用的工程师;
- 对 WAF、Bot Management、会话风控和可解释评分感兴趣的 Python 读者。
你不需要机器学习基础。本文会先使用透明的加权规则解释原理,再说明生产系统为何会加入基线、异常检测与模型。示例仅用于本地学习和授权防御,不针对真实第三方站点进行自动化测试。
本文验证时间为 2026 年 8 月。
一、为什么很多机器人不会看到验证码
不少人以为风控流程是:
检测到机器人 → 弹验证码 → 验证失败 → 拦截
真实系统往往更接近:
请求与客户端信号
↓
按会话、账号、设备或时间窗口聚合
↓
计算风险分与原因
↓
低风险:放行
中风险:记录、降速或后台挑战
较高风险:显式验证
高风险:拦截或人工复核
验证码只是处置动作之一,不是检测本身。如果一个会话始终表现正常,系统没有必要打断用户;如果证据已经充分,系统也可能直接限速或拦截,而不让攻击者反复消耗验证码资源。
二、单次请求看不出的东西,会话可以看出来
假设有两次请求:
GET /products/1001
GET /products/1002
只看请求本身,很难判断它们来自真人还是程序。把 60 秒内的请求放在一起,差异可能出现:
正常会话示意
/home
└─ 4.8 秒后 /search?q=keyboard
└─ 7.2 秒后 /products/1001
└─ 19.6 秒后 /cart
可疑会话示意
/products/1001 00:00.000
/products/1002 00:00.500
/products/1003 00:01.000
/products/1004 00:01.500
...
第二个会话不一定恶意,它也可能是监控、测试或无障碍工具。但它至少出现了值得进一步检查的特征:
- 请求间隔过于固定;
- 短时间遍历大量不同资源;
- 没有首页、搜索或列表页等常见前置路径;
- 行为持续时间和访问深度不符合普通交互。
风控的关键不是某个“神奇字段”,而是把若干弱信号组合成更可靠的判断。
三、行为风控的四层结构
第 1 层:原始事件
原始事件是还没有经过聚合的数据:
- 请求时间;
- URL、方法、状态码;
- 点击、触摸、滚动;
- 页面进入和离开;
- 登录、搜索、加购、支付等业务动作;
- 挑战是否通过;
- TLS/HTTP 指纹、HTTP 版本等客户端信号。
原始事件越多不代表系统越好。无业务价值的高精度轨迹会带来性能、隐私和合规负担。
第 2 层:聚合特征
系统通常按会话和时间窗口生成特征,例如:
request_count_60s 60 秒请求数
unique_paths_60s 60 秒不同路径数
interarrival_cv 请求间隔变异系数
navigation_violation_rate 不符合常见路径的比例
client_consistency 客户端跨层一致性
challenge_failed 挑战是否失败
第 3 层:风险评分与原因
评分器把多个特征转成风险分,同时保留可解释原因:
{
"score": 75,
"reasons": [
"60 秒请求过多",
"请求间隔过于规律",
"页面跳转异常"
]
}
第 4 层:策略处置
评分不是最终动作。业务策略还要考虑:
- 当前接口价值;
- 用户是否登录;
- 是否涉及支付、提现、换绑;
- 历史信誉;
- 当前攻击态势;
- 验证成本与用户流失。
同样 60 分,访问公开帮助页可能只记录,批量尝试登录则可能触发 MFA 或账号保护。
四、常见行为信号
1. 请求速度与速度变化
简单限速只看“每分钟多少次”。行为风控还会看:
- 是否突然从低频变成高频;
- 是否在固定时间点爆发;
- 是否跨多个 IP 保持相同节奏;
- 是否只集中访问高价值接口;
- 是否长时间卡在阈值下方运行。
因此,固定阈值只能作为一层保护,不能单独解决分布式或低频自动化。
2. 请求间隔分布
设相邻请求间隔为:
Δt = [0.50, 0.49, 0.51, 0.50, 0.50]
可以计算变异系数:
CV = 标准差 / 平均值
CV 很小,说明间隔非常规律。但它只能算弱信号:任务调度器、健康检查和合法 API 客户端也可能高度规律;真人连续点击也可能在短窗口中出现低 CV。
3. 页面导航与业务状态机
电商网站的常见路径可能是:
首页 → 搜索/分类 → 商品详情 → 购物车 → 结算
如果一个新会话直接从数万个详情页开始顺序遍历,或跳过必须完成的业务步骤,风险通常高于正常会话。
这种检测不必收集鼠标坐标,只需要业务事件和状态转移即可,往往更可解释。
4. 客户端跨层一致性
可以检查:
- User-Agent 声称的浏览器是否与 TLS/HTTP 能力大体一致;
- 同一会话的语言、时区、屏幕和协议特征是否无原因突变;
- Cookie、挑战令牌和会话标识是否保持合理连续性;
- 浏览器端信号与服务端观察是否矛盾。
这里的目的不是追踪个人,而是发现同一会话内部的明显冲突。上一篇《JA3 已经不够用了:JA4/JA4H 风控指纹原理与本地实验》负责解释协议层信号,本文负责把它们放进会话评分。
5. 鼠标、触摸与交互节奏
HUMAN 的官方检测概览提到点击、触摸、节奏和时序等交互异常,也会结合浏览器、移动端和网络信号。
不过,“鼠标轨迹像直线就是机器人”是过度简化。以下情况都可能造成误判:
- 键盘操作用户;
- 屏幕阅读器;
- 触控板、触摸屏或远程桌面;
- 运动能力受限用户;
- 企业自动化与回归测试;
- 浏览器节能、后台标签页和网络延迟。
因此,交互信号应当被聚合、降精度和限期保存,不应成为单一封禁条件。
6. 群体与协同行为
单个客户端可能频率不高,但一万个客户端同时:
- 请求相同路径集合;
- 使用高度相似的时间节奏;
- 在同一秒开始登录或抢购;
- 对账号列表进行分片尝试。
这时需要从单会话上升到群体分析。AWS 官方资料将行为分析、异常检测和对分布式协同行为的机器学习检测列为 Bot Control 的高级能力。
五、为什么要输出“原因”,而不只输出分数
只有 score=78 的系统很难运营。安全人员和客服会追问:
- 为什么判高风险?
- 是频率、路径,还是挑战失败?
- 哪个规则最近导致误报上升?
- 用户通过二次验证后应解除哪项限制?
因此,一个实用评分结果至少应包含:
{
"score": 78,
"level": "high",
"action": "challenge",
"reasons": [
{"code": "RATE_HIGH", "weight": 25},
{"code": "INTERVAL_REGULAR", "weight": 20},
{"code": "NAVIGATION_ANOMALY", "weight": 20}
],
"model_version": "demo-2026-08"
}
原因码便于灰度、审计、客服解释和后续复盘。
六、Python 本地实验:实现一个可解释评分器
下面的程序不采集鼠标轨迹,也不需要数据库。它接受聚合后的会话指标,输出风险分、原因和建议动作。
请保存为 behavior_score.py:
from dataclasses import dataclass
from typing import Dict, List, Tuple
@dataclass(frozen=True)
class SessionMetrics:
request_count_60s: int
unique_paths_60s: int
interarrival_cv: float
navigation_violation_rate: float
client_consistency: float
challenge_failed: bool = False
def clamp(value: float, low: float, high: float) -> float:
return max(low, min(value, high))
def validate(metrics: SessionMetrics) -> None:
if metrics.request_count_60s < 0:
raise ValueError("request_count_60s 不能为负数")
if metrics.unique_paths_60s < 0:
raise ValueError("unique_paths_60s 不能为负数")
bounded = {
"interarrival_cv": metrics.interarrival_cv,
"navigation_violation_rate": metrics.navigation_violation_rate,
"client_consistency": metrics.client_consistency,
}
for name, value in bounded.items():
if not 0.0 <= value <= 1.0:
raise ValueError(f"{name} 必须位于 0.0 到 1.0")
def score_session(metrics: SessionMetrics) -> Dict[str, object]:
"""教学演示:阈值必须用自有业务基线重新校准。"""
validate(metrics)
reasons: List[Tuple[str, int]] = []
if metrics.request_count_60s >= 120:
reasons.append(("RATE_VERY_HIGH", 35))
elif metrics.request_count_60s >= 60:
reasons.append(("RATE_HIGH", 25))
elif metrics.request_count_60s >= 30:
reasons.append(("RATE_ELEVATED", 10))
if metrics.unique_paths_60s >= 40:
reasons.append(("PATH_ENUMERATION", 20))
elif metrics.unique_paths_60s >= 20:
reasons.append(("MANY_UNIQUE_PATHS", 10))
# 请求数量太少时,CV 没有足够统计意义。
if metrics.request_count_60s >= 10 and metrics.interarrival_cv < 0.08:
reasons.append(("INTERVAL_TOO_REGULAR", 20))
if metrics.navigation_violation_rate >= 0.70:
reasons.append(("NAVIGATION_HIGHLY_ANOMALOUS", 25))
elif metrics.navigation_violation_rate >= 0.40:
reasons.append(("NAVIGATION_ANOMALOUS", 15))
if metrics.client_consistency < 0.40:
reasons.append(("CLIENT_SIGNALS_CONFLICT", 20))
elif metrics.client_consistency < 0.70:
reasons.append(("CLIENT_SIGNALS_WEAK", 10))
if metrics.challenge_failed:
reasons.append(("CHALLENGE_FAILED", 30))
score = min(100, sum(weight for _, weight in reasons))
if score >= 80:
level, action = "critical", "block_or_manual_review"
elif score >= 60:
level, action = "high", "challenge"
elif score >= 30:
level, action = "medium", "monitor_and_limit"
else:
level, action = "low", "allow"
return {
"score": score,
"level": level,
"action": action,
"reasons": [
{"code": code, "weight": weight}
for code, weight in reasons
],
"model_version": "demo-2026-08",
}
if __name__ == "__main__":
samples = {
"normal_reader": SessionMetrics(
request_count_60s=8,
unique_paths_60s=5,
interarrival_cv=0.55,
navigation_violation_rate=0.05,
client_consistency=0.95,
),
"regular_enumerator": SessionMetrics(
request_count_60s=90,
unique_paths_60s=75,
interarrival_cv=0.02,
navigation_violation_rate=0.82,
client_consistency=0.45,
),
"qa_monitor": SessionMetrics(
request_count_60s=35,
unique_paths_60s=8,
interarrival_cv=0.03,
navigation_violation_rate=0.10,
client_consistency=0.96,
),
}
for name, metrics in samples.items():
print(name, score_session(metrics))
运行:
python3 behavior_score.py
你会看到三类结果:
- 普通阅读会话应接近低风险;
- 规律遍历大量路径的会话会进入高风险;
- 合法 QA/监控虽然间隔规律,但如果频率、路径和客户端一致性正常,不应仅凭一个信号直接封禁。
这里的阈值是为了展示推理过程,不是通用生产参数。
七、从原始时间戳计算间隔变异系数
如果已有请求时间戳,可以这样生成 interarrival_cv:
from statistics import mean, pstdev
from typing import Iterable
def interarrival_cv(timestamps: Iterable[float]) -> float:
values = sorted(timestamps)
if len(values) < 3:
return 1.0
intervals = [
right - left
for left, right in zip(values, values[1:])
if right > left
]
if len(intervals) < 2:
return 1.0
average = mean(intervals)
if average == 0:
return 0.0
return pstdev(intervals) / average
print(interarrival_cv([0.0, 0.5, 1.0, 1.5, 2.0]))
print(interarrival_cv([0.0, 0.7, 2.1, 2.4, 5.8]))
第一组的间隔高度规律,CV 接近 0;第二组波动更大。
注意三个边界:
- 样本太少时不要下结论;
- 网络抖动会改变服务端看到的间隔;
- 合法定时任务本来就可能非常规律。
八、规则评分、异常检测和机器学习怎么选
1. 规则评分
优点:
- 原因清楚;
- 容易上线和回滚;
- 适合已有明确滥用模式;
- 客服和安全运营容易理解。
缺点:
- 容易堆积规则;
- 固定阈值难适配不同业务;
- 攻击者可能长期贴着阈值运行。
2. 基线与异常检测
系统先学习“本业务通常是什么样”,再寻找偏离:
- 同一接口历史请求分布;
- 同一账号常见设备、地区和时间;
- 同一活动开始前后的流量变化;
- 相似会话的聚类差异。
优点是能适配业务,缺点是活动、版本发布、爬虫收录或营销流量也会造成正常基线突变。
3. 监督或半监督模型
模型可以组合大量弱信号,但前提是:
- 有可靠标签;
- 能监控数据漂移;
- 保留模型版本和特征版本;
- 能解释关键原因;
- 有回滚和人工复核路径。
成熟系统通常不是“规则或模型二选一”,而是:
硬性安全规则
+
业务基线与异常检测
+
模型风险分
+
策略引擎
九、评分之后如何处置
推荐使用渐进式处置:
| 风险等级 | 建议动作 | 目的 |
|---|---|---|
| 低 | 放行并保留必要指标 | 减少正常用户摩擦 |
| 中 | 记录、降速、收紧配额 | 获取更多证据,控制成本 |
| 高 | 后台挑战、CAPTCHA、MFA | 验证客户端或用户 |
| 极高 | 临时阻断、冻结高价值动作、人工复核 | 保护账号和资金 |
为什么不建议一上来永久封禁
- 动态 IP 可能被多人共享;
- 企业代理会汇聚大量正常用户;
- 浏览器升级会改变指纹;
- 无障碍工具可能产生特殊行为;
- 搜索引擎、监控、合作方 API 都可能是合法自动化;
- 风险模型本身可能出现数据漂移。
处置应尽量可恢复,并记录:触发原因、开始时间、失效时间、解除条件和策略版本。
十、如何评估风控是否真的有效
只看“拦截请求数”会误导团队。至少需要同时关注:
- 误报率;
- 挑战通过率;
- 正常用户转化率变化;
- 高价值接口的滥用损失;
- 客服投诉与申诉恢复时间;
- 规则命中后真实恶意占比;
- 延迟与前端性能开销;
- 绕过后攻击是否转移到其他接口。
一个拦截量很高的规则,可能只是把正常监控或搜索引擎全部挡掉。真正的目标是降低业务损失,同时控制用户摩擦。
十一、隐私、无障碍与合规边界
行为风控有能力收集很多数据,但“能采集”不等于“应该采集”。生产设计建议:
- 优先使用服务端已有业务事件;
- 只采集与明确安全目的有关的客户端信号;
- 对坐标、时间和设备字段做必要的降精度;
- 设置较短保存期限;
- 将原始数据与分析结果分权访问;
- 不把行为信号用于无关画像;
- 为键盘用户、屏幕阅读器和其他无障碍场景提供替代验证;
- 对封禁和高价值动作保留申诉、人工复核与恢复路径。
风险分是系统的判断,不是对人的定性。
十二、常见错误
错误 1:把鼠标直线当成机器人
这会误伤触摸屏、键盘用户、远程桌面和辅助技术。轨迹只能是一个弱信号。
错误 2:所有页面用同一个阈值
帮助页、搜索、登录、支付和提现的风险不同,应当按业务动作配置策略。
错误 3:先封禁,再观察
正确顺序通常是离线回放、观察模式、小流量灰度、渐进式处置和持续复盘。
错误 4:只看单会话,不看群体
分布式攻击会把频率拆散到大量 IP 和设备,需要分析账号、路径、时间和行为的协同性。
错误 5:只存分数,不存原因和版本
没有原因码和版本,误报无法定位,模型更新也无法审计。
十三、生产上线清单
- 明确要保护的业务动作,而不是泛泛“防机器人”;
- 为每个指标写清单位、窗口、缺失值和数据来源;
- 用自有流量建立基线,不照搬文章阈值;
- 保留原因码、评分版本和策略版本;
- 先观察,再灰度处置;
- 将协议指纹、行为、账号和挑战结果联合使用;
- 对合法 API、监控、搜索引擎和合作方建立身份与配额;
- 评估无障碍、隐私、延迟和客户端性能;
- 为高风险动作准备二次验证和人工复核;
- 定期检查漂移、误报和规则失效。
十四、总结
不弹验证码也能识别机器人,原因不是系统掌握了某个万能特征,而是它观察了一个会话在时间、路径、客户端和业务状态上的一致性。
完整链路可以概括为:
事件 → 时间窗口聚合 → 特征 → 风险分与原因 → 分级处置 → 效果复盘
单次请求通常只有弱证据;多个相互独立的信号在会话中同时异常,才适合提高处置强度。协议指纹告诉我们“客户端怎样建立连接”,行为风控告诉我们“这个会话怎样使用业务”,两者结合才更接近现代 Bot Management。
相关内容
参考资料
- HUMAN:Detection Overview
https://docs.humansecurity.com/applications/bd-detection-overview - AWS:Choosing and configuring Bot Control for your use case
https://docs.aws.amazon.com/waf/latest/developerguide/waf-bot-control-use-cases.html - AWS:Advanced analysis controls for managing bots
https://docs.aws.amazon.com/prescriptive-guidance/latest/bot-control/advanced-analysis-controls.html - Google Cloud:Interpret reCAPTCHA assessments
https://cloud.google.com/recaptcha/docs/interpret-assessment-website - FoxIO:JA4+ 官方仓库
https://github.com/FoxIO-LLC/ja4
合规说明:本文用于自有系统和授权环境中的防御设计。请勿将示例用于规避第三方网站的访问控制;部署行为分析时,应遵守隐私、数据保护、无障碍和服务条款要求。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)