跳转到内容

搜索结果页的客户端风控:落地到点击签名的一生

识别与对抗那几篇,讲的是检测方用什么原理把人和机器分开——维度诅咒、物理自洽、统计矩。但那些原理,大多站在服务端 / 离线的视角:引擎汇总海量请求,事后判别。

这一部换一个视角,也是更贴身的一个:客户端。当你打开国内最大搜索引擎的一个结果页,页面里加载的,远不只是那十条蓝色链接。还有一套脚本,从你落地的那一刻起,就开始一刻不停地审视你——直到你离开,甚至你下次再来。

这一篇是这套脚本的总解剖图。它对称于第一部的「一次搜索的一生」:那讲的是一个 query 在引擎内部的旅程,这讲的是,在结果页里被观测的旅程。


一、结果页是一个程序,不是一张纸

Section titled “一、结果页是一个程序,不是一张纸”

先纠正一个心智模型。大多数人以为搜索结果页是这样的:服务器把结果渲染成 HTML,浏览器把它画出来,你看、你点,如此而已——一张排好版的纸。

错。结果页是一个持续运行的程序。 它加载后,一套客户端脚本随即启动,目标只有一个:

在你与页面交互的全过程中,持续采集信号,判断「你是不是一个真人」,并把证据源源不断地上报回去。

这套脚本是服务端风控前哨。服务端只能看到你的请求——IP、UA、时间。而客户端脚本能看到请求里没有的一切:你的手指怎么滑动(PC 上则是鼠标怎么移动)、点击前犹豫了多久、页面在你的设备上渲染得多快、你有没有传感器、你的浏览器内核是不是真的。这些,才是判别真人最锋利的信号,而它们只在你的浏览器里、只在这个页面活着的时候,才能被采到。

所以理解客户端风控,第一步是理解它的生命周期:它在什么时刻、做什么、采什么、报什么。


把这套脚本从你打开页面到离开的完整历程摊开,是这样五个阶段:

结果页的一生:从落地体检到跨会话续传① 同步初始化落地即体检指纹·存储·策略② 挂监听触摸·键盘·滚动全埋点③ 周期采样探针·轨迹·心跳后台不停④ 交互触发点击被签名反应时·坐标⑤ 卸载续传flush + 持久化下次补传页面加载你离开 →跨会话续传:你走了它还在,下次再来接着缝

注意最后那条金色回环——它的观测不因你关掉页面而终止。下面逐段看这五个阶段各在做什么。


脚本加载的同步阶段——你还没来得及动一下鼠标——它已经完成了一次「环境体检」:

  • 采一遍指纹:设备、浏览器、屏幕、硬件的特征被迅速采集(这些指纹如何构成身份、如何被识破,见浏览器指纹篇,此处只关心「它在落地瞬间就采了」)。
  • 探一遍能力:本地存储可不可用、容量多大,Cookie 开没开,各种 API 在不在。一个连本地存储都异常的环境,本身就是可疑信号。
  • 领一份策略:服务端在下发页面时,会附带一组「策略」——是否信任你、给你多少监控力度。异常的指纹,可能一份策略都领不到。
  • 打一组时间戳:记录导航、DNS、渲染各节点的基线时刻,为后面的性能判别(第三部下一篇)铺底。

这一段最该记住的一点是时机

这里还埋着一个反直觉的陷阱:这套体检依赖本地存储来落地它的状态。如果你的环境把本地存储禁用或改坏了(某些「隐私」或魔改浏览器),体检会初始化失败,整个风控链条断裂——页面进入一种「僵尸」状态。而一个埋点全断的页面,对服务端而言不是「更干净」,而是更可疑。想通过「什么都不给它采」来隐身,反而点亮了自己。


四、第二阶段·把你的一举一动接上线

Section titled “四、第二阶段·把你的一举一动接上线”

体检过后,脚本挂上一整套事件监听:触摸的按下、移动、抬起,滑动与滚动,键盘输入,焦点进出(在 PC 上,则对应鼠标的移动、点击、悬停)。

它用的是全埋点手法——在文档的顶层监听,靠事件冒泡,捕获页面里任何元素上的点击,而不需要给每个链接单独绑定。于是从这一刻起:

你在这个页面里的每一次移动、每一次点击、每一次滚动、每一次输入,都有一双眼睛在记录。

这些原始事件本身还不是判决,它们是素材——喂给下一阶段的采样与上报,也喂给行为建模那套「人是带结构的随机过程」的判别。


这是最容易被忽略、也最能体现「一刻不停」的一段:脚本不等你交互,它自己按节奏在跑。

它至少在三个时间尺度上同时工作:

  • 毫秒级探针:高频地检查运行环境是否「正常」——比如反复测量脚本执行的卡顿程度。这既是性能监控,也是环境判别(下一篇细讲)。
  • 秒级采样:以固定的节流间隔,对你的触摸/滑动轨迹、滚动位置做抽样记录(PC 上则是鼠标轨迹),攒成一条可回放的行为流。
  • 心跳上报:每隔一段时间,把攒下的数据打包送回服务端,附带「你已经停留了多久」。

三个时间尺度,同时盯着你毫秒探针秒级采样心跳上报↑ 间隔递增(非固定、非线性)——固定间隔本身就是破绽到达离开 →→ 跨会话续传从你到达到你离开,三个尺度同时运转,无一刻停歇

这里藏着一个精彩的细节,也预告了下一篇的主题:心跳的间隔不是固定的。它往往是一条递增、且增速本身还在变的序列(像斐波那契那样)。为什么要这么麻烦?因为固定间隔本身就是一种机器签名——真实系统里几乎没有严格周期的东西。这与反作弊篇里「均匀是一种异常」、行为篇里「太规整就露馅」是同一条道理,只不过这次它用在了脚本自己的上报节奏上——连监控者自己,都要把自己的心跳伪装得不规则。


六、第四阶段·点击是一次被签名的事件

Section titled “六、第四阶段·点击是一次被签名的事件”

当你真的点了一个结果,在你眼里那只是一次跳转。在脚本眼里,那是一个需要取证并签名的事件。

它会采集这次点击的微观特征:按下到抬起持续了多久(真人有毫秒级的按压时长,脚本调用往往是 0)、点在了元素的什么相对位置(真人散布,脚本爱点正中)、以及一个尤其致命的量——从注意到目标、到手指(或指针)真正落下之间的那段「反应时间」。真人从看到到点下,有一段百来毫秒的思考停顿;脚本常常是定位与点击同时发生,反应时间为零。

更狠的是,这些特征会被绑定签名:把点击特征连同当前的搜索词、目标链接一起,算出一个校验值随点击上报。你要是想伪造点击日志、或者偷换了搜索词,签名对不上,后端一眼看穿。这让「凭空捏造一条漂亮的点击记录」变得几乎不可能——点击的证据,和它的上下文被焊死在了一起。

(点击特征背后的运动学与反应时约束,属于行为建模;这里只强调架构层面的一点:在客户端,点击不是一个动作,是一份带签名的证词。


大多数程序在页面关闭时就结束了。这套脚本不是。

在页面卸载的那一刻,它抓住最后的时间窗,用一种即使页面正在销毁也能可靠送出的方式(信标式上报),把还没发完的数据 flush 回去。实在来不及发的,就存进本地——等你下一次再打开它的页面,先把上次的尾巴补传上去。

于是你的行为不是一次会话一次会话孤立的,而是被跨会话缝合成一条连续的档案


八、为什么这套架构如此难对付

Section titled “八、为什么这套架构如此难对付”

回头看这五个阶段,会发现客户端风控真正的威力,不在任何单一检测,而在它的结构

它把审视嵌入到页面运行的每一毫秒、每一个物理约束里。不是进门时查一次证,而是从你落地、到你交互、到你离开、到你再来,全程、多尺度、不间断地观测。

这给攻击者出了一道维度诅咒时间版难题:要伪装成真人,你不能只在「被点名的那一刻」表现得像人,而要在整条生命周期的每一个阶段、每一个时间尺度上都不露破绽——落地体检要过、行为流要像、心跳节奏要自然、点击证词要真、跨会话轨迹要连贯自洽。任何一个阶段的一处失真,都会被这条连续的记录捕捉到。防守方只需在这条时间线上抓住一个不自然的瞬间。

后面三篇,就沿着这条生命周期往深里挖:


搜索结果页不是一张排好版的纸,而是一个一刻不停运行的审视程序。它的生命周期有五段:落地即体检(你还没动,环境已被验身)、挂监听(你的一举一动接上线)、周期采样(毫秒探针 / 秒级采样 / 递增心跳,后台不停,连自己的上报节奏都伪装成不规则)、交互触发(点击是一份被签名、绑定上下文的证词)、卸载续传(你走了它还在,跨会话把你缝成一条连续轨迹)。它的威力不在单点检测,而在把审视嵌进页面运行的每一毫秒——这是维度诅咒的时间版:要蒙混,你得在整条时间线上处处不露破绽;而它,只需逮住一个不自然的瞬间。