上周朋友找我调一个UI自动化脚本,卡在南航登录环节的滑块验证上,回放录屏一看,每次都是拖到一半被判定异常,然后提示重新验证。索性把这一套滑块验证从前端交互到服务端校验逻辑完整拆了一遍。
南航这种滑块验证方案,和市面主流的人机验证产品在架构上同根同源,都是"前端采集轨迹 + 服务端行为判定"的模式。把它当作样本分析,缺口识别、轨迹构造、风控检测这些核心算法也就一并理清了。这篇文章把整个拆解过程整理出来,从图像处理到行为模拟,再到服务端风控视角的对抗逻辑,适合做自动化测试、前端风控、安全评估的同行参考。我尽量把原理讲透,代码能直接复用的也会标注清楚。
1. 滑块验证的整体架构与南航场景拆解
1.1 一次完整滑块验证的请求链路
滑块验证看起来只是"拖动小图到缺口"一个动作,背后其实是三段式链路配合:
第一步,前端向后端发起验证码初始化请求,服务端返回两张图:一张带缺口的背景图,一张滑块小图,同时下发一个唯一标识这次会话的 token。这张背景图里的缺口位置是服务端随机生成的,理论上只有服务端知道精确坐标。
第二步,用户在浏览器里拖动滑块,前端 JavaScript 实时监听鼠标或触屏的移动事件,记录一系列轨迹点,每个点包含 x 坐标、y 坐标和时间戳。松手后,前端把这些轨迹数据连同设备环境信息一起加密,作为验证请求提交。
第三步,服务端拿到轨迹数据后,用一套风控模型来判断"这是真人还是程序"。判断通过则下发通过凭证,前端用这个凭证继续业务请求;判断不通过则重新出题。
这个链路最关键的一点是:服务端根本不关心你到底拖得准不准,它关心的是"你拖的这个过程像不像人"。缺口位置识别得越准,轨迹模拟得越拟真,通过率才越高。这也决定了滑块算法分析的核心工作集中在两块:图像缺口定位和行为轨迹构造。
1.2 南航场景的特殊性:为什么这块滑块值得单独分析
南航的滑块验证有几个特点,让它在同类方案里很有代表性,也让它比其他网站的滑块更值得拆。
第一个特点是图片质量高、背景复杂。南航背景图通常选用飞机、机场、风景插画这类素材,颜色丰富、纹理细节多,这和很多网站用纯色渐变背景不一样。纹理复杂直接导致图像缺口识别难度上升,边缘误检率比纯色背景高出一截。
第二个特点是交互控件是移动端优先设计。南航的滑块既支持手机触屏滑动,也支持 PC 鼠标拖动,两种输入方式的行为特征差异很大——触屏拖动没有鼠标悬停抖动,PC 拖动有更明显的小幅修正。做轨迹模拟时,必须区分设别类型生成不同特征的轨迹。
第三个特点是业务场景触发频率高、环境要求严。登录、购票、查询会员信息都有可能触发验证,而且南航对 IP 和账号环境比较敏感。同一个 IP 短时间内频繁触发验证,风控系统会直接提高验证难度,表现为背景图干扰加重、可拖拽时间缩短。
分析这类滑块验证,不能只盯着一套固定的缺口识别和轨迹生成方案,还得考虑不同终端、不同网络环境、不同触发频率下的动态变化。这也是我把这三次核心算法分开讲的原因——每一块都能独立优化,也都有自己独立的坑。
2. 核心算法模块一:缺口识别与图像处理
2.1 缺口识别的三种技术路线对比
想拖动滑块,先得知道往哪拖,所以缺口识别是整套流程里的第一个技术关卡。目前主流方案有三类:
第一类是传统模板匹配。把滑块小图当作模板,在背景大图上搜索最相似的区域。这种方案实现简单、不需要训练数据,几分钟就能跑通。缺点是遇到复杂背景容易误匹配,而且要求滑块小图的轮廓和背景里的缺口形状高度一致。南航这种插画风格的背景,纯模板匹配经常会匹配到云朵、机身弧线这些相似纹理上。
第二类是边缘检测 + 轮廓匹配。先用 Canny 或 Sobel 算子提取背景图和滑块图的边缘信息,再做模板匹配或轮廓比对。因为边缘信息去掉了颜色干扰,只保留结构轮廓,对复杂背景的抗干扰能力明显强于直接像素匹配。这也是我实际项目里用的主力方案。
第三类是深度学习目标检测。用目标检测或图像分割模型直接回归缺口框的位置,常见做法是 YOLO 系列加少量标注数据训练,或者用 U-Net 做像素级分割。深度学习方案泛化能力最强,遇到已见过的背景风格几乎能做到像素级精准,但缺点是需要收集样本、标注数据、训练模型,工程成本比前两类高一个量级。
我个人在分析南航这类验证码时的建议是:先走边缘检测 + 模板匹配的路线,把流程跑通,看识别精度瓶颈在哪,再决定要不要上深度学习。大部分场景下,传统视觉方案调好了精度完全够用,没必要一上来就上模型。
2.2 边缘检测与模板匹配的工程实现
下面是一套可运行的缺口定位实现,用 Python + OpenCV 写,核心步骤就四步:预处理、边缘提取、模板匹配、坐标换算。
import cv2 import numpy as np def locate_gap(bg_path, fg_path): # 背景图预处理:转灰度、高斯模糊降噪 bg = cv2.imread(bg_path) bg_gray = cv2.cvtColor(bg, cv2.COLOR_BGR2GRAY) bg_blur = cv2.GaussianBlur(bg_gray, (5, 5), 0) # 滑块图处理:优先用alpha通道,轮廓更干净 fg = cv2.imread(fg_path, cv2.IMREAD_UNCHANGED) if fg.shape[2] == 4: alpha = fg[:, :, 3] fg_blur = cv2.GaussianBlur(alpha, (3, 3), 0) _, fg_mask = cv2.threshold(fg_blur, 127, 255, cv2.THRESH_BINARY) else: fg_gray = cv2.cvtColor(fg, cv2.COLOR_BGR2GRAY) _, fg_mask = cv2.threshold(fg_gray, 127, 255, cv2.THRESH_BINARY) # Canny边缘检测 bg_edge = cv2.Canny(bg_blur, 100, 200) fg_edge = cv2.Canny(fg_mask, 100, 200) # 模板匹配:用滑动窗口计算相似度 result = cv2.matchTemplate(bg_edge, fg_edge, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc = cv2.minMaxLoc(result) # max_loc是滑块图左上角在背景图中的坐标 return max_loc[0], max_val这里有三个细节值得展开。
第一,为什么先做高斯模糊。背景图经过服务端压缩,会有 JPEG 压缩噪声,如果不做模糊直接提边缘,会出现大量细碎的假边缘。但模糊核也不能太大,否则边缘位置会偏移。我实测背景图是 680 像素宽时,高斯核用 5x5 比较合适,再大会把缺口边界磨掉一两个像素。
第二,宁可多算两步,把滑块图的轮廓做干净。滑块小图通常带透明通道,直接用 RGB 像素做模板匹配会把透明区域的黑色底也算进去,导致匹配结果严重偏差。用 alpha 通道提取不透明区域轮廓,是处理滑块模板的关键技巧,很多第一次写识别脚本的人都会漏掉这一步。
第三,模板匹配返回的是像素级坐标。不同网站的滑块容器布局差异很大,小图左侧可能有一段透明留白,实际滑动距离不等于模板匹配的 x 坐标。做坐标换算时,要看前端代码里滑块初始位置和背景图左边界的关系,通常需要减去一个固定偏移量。分析南航的页面时,我花了些时间在控制台里核对滑块小图的 CSS 位置,最后确认需要减去初始 left 偏移量才能得到真实拖动距离。
2.3 识别误差分析与精度调优
缺口识别不是拖过去就行,误差太大会直接触发风控的"位置校验",试几次就会被拦截。风控系统会计算最终落点与真实缺口的距离,真人拖动通常有 1 到 3 像素的随机误差,误差为 0 反而像机器。所以识别模块的两个优化方向:一是提高识别基准值的准确度,二是把识别值加上随机偏移再交给轨迹生成模块。
工程调优时,我总结了几条实用经验:
- 多尺度匹配:背景图在不同屏幕分辨率下会被 CSS 缩放,宽度可能不是原始像素宽度。先读前端代码里背景图的实际渲染宽高,对模板做等比缩放,否则误差可能达到几十像素。
- 双阈值 Canny 的自适应:固定用 100 和 200 在某种背景下效果好,换到另一种背景效果可能很差。可以先用 Otsu 二值化找梯度分布,再动态设置 Canny 阈值,这类自适应调整能明显降低复杂插画背景的误检率。
- 置信度判断:matchTemplate 返回的 max_val 可以当作置信度。如果低于 0.5,说明当前模板匹配结果不可靠,直接重新加载验证码,不要硬识别。这个"放弃重试"策略比强行调参省时间得多。
3. 核心算法模块二:拟人轨迹的构造原理
3.1 机器轨迹和人类轨迹的本质差异
缺口定位解决的是"往哪拖",轨迹生成解决的是"怎么拖"。服务端风控对轨迹的检测,本质上是一个二分类问题:把轨迹特征输入模型,判断是真人操作还是机器生成的。
真人和机器生成的轨迹,差异体现在三个层面。
第一层是速度曲线。真人拖动不是匀速的,而是"慢启动—加速—减速—微调"的过程。手指或鼠标从静止到运动,加速度是渐变的,不会瞬间爆发到高速。而最简单的机器轨迹是匀速直线,速度曲线是一条水平线,这在风控模型里属于一眼假的特征。
第二层是时间序列的规律性。真人拖动的时间间隔不是完全均匀的,两次采样之间可能有 10ms 到 50ms 的波动。机器的定时器触发则非常均匀,间隔几乎恒定,频域分析下会出现明显的周期峰。
第三层是噪声和抖动。真人手部控制有天然的生理抖动,哪怕意图保持直线,轨迹也不会是完美直线,会有几个像素的横向偏移和方向修正。机器轨迹往往要么过于干净平滑,要么随机噪声幅度过大、频率过高,这两种极端都容易被识别。
理解了这三层差异,轨迹生成算法的思路就清晰了:构造一条速度曲线符合物理规律、时间序列有自然波动、空间位置带适度噪声的轨迹。
3.2 轨迹生成器的设计与实现
实际工程里,我倾向于用三次缓出曲线 + 随机噪声的组合方案。缓出曲线模拟了"开始慢、中间快、结束慢"的物理过程,随机噪声模拟了手部抖动和修正行为。
import numpy as np import random def generate_track(distance): # 人类观察缺口到完成拖动,通常需要0.8到1.6秒 duration = random.uniform(0.8, 1.6) # 以20ms为采样间隔 steps = int(duration * 50) # 三次缓出曲线:1 - (1-t)^3 t = np.linspace(0, 1, steps) eased = 1 - np.power(1 - t, 3) # 横向位移 = 缓出曲线 * 目标距离 + 正态抖动 track_x = eased * distance # 手部生理抖动:幅度0.4个像素左右,频率较高 jitter = np.random.normal(0, 0.4, steps) track_x = track_x + jitter # 最后的对齐阶段:真正拖动结束时会有小幅修正 track_x[-1] = distance track_x[-2] = distance + random.uniform(-1.2, 0) # 纵向位移:拖动不是完美水平线,加入小幅度漂移 track_y = np.random.normal(0, 1.0, steps) track_y[-1] = 0 track = [] for i in range(steps): track.append({ "x": round(float(track_x[i]), 2), "y": round(float(track_y[i]), 2), "t": i * 20 }) return track这套生成器有几个关键的随机项,我逐个解释。
时间总长设定在 0.8 到 1.6 秒之间,这是真人完成一次滑块拖动的常见区间。低于 0.5 秒太快,超过 2.5 秒又显得犹豫,都会被模型标记。
三次缓出曲线的物理含义是:后半段的位移增量逐渐减少,对应手指接近目标时减速。这个模型比一次函数匀速拖动要真实得多,也比二、四次曲线的生硬加速更接近手指运动。
正态抖动幅度是关键参数。我试过 0.2 到 1.5 像素的不同幅度,最后落在 0.4 最好——太小轨迹过于平滑,太大则显得手在剧烈颤抖。幅度应结合滑块宽度调节,南航这类 300 像素宽的滑块,0.4 到 0.8 都算合理区间。
终点修正模拟了"第一下拖过头或没到位,再微调一下"的动作。真人对准缺口很少一步到位,通常误差 1、2 个像素,甚至会有往回拖几像素的修正。机械生成的轨迹为了准确往往落点误差为 0,这反而成了破绽。
3.3 轨迹质量的自检方法
生成轨迹之后,别急着提交验证,先看一眼轨迹曲线是否符合人类特征。我平时会做三个快速检查:
一是画位移-时间图,曲线应该是平滑的 S 形,而不是直线或锯齿。如果某一段出现了平台期,说明轨迹疑似中途停滞,真人拖动不会完全静止再突变。
二是算速度曲线,速度应该先升后降,峰值出现在拖动中段。如果速度曲线有多个峰值且幅度大,说明轨迹充满反复修正,这种轨迹在真人的快速拖动场景里不常见。
三是检查落点误差,最终落点应该和缺口位置有 1 到 3 像素的随机误差。每次提交都精确命中缺口中心的轨迹,风控模型会给出很高的人为标记优先级。
另外一个容易被忽略的质量指标是轨迹点数量。真人拖动每秒采样的点数受限于硬件上报频率,鼠标和触屏常见是 60 到 120Hz,对应每 20ms 一个点左右。如果生成的轨迹点密集到每 5ms 一个点,那就太不正常了。所以我生成轨迹时固定用 20ms 间隔,这也和多数浏览器事件监听的实际频率对上。
4. 核心算法模块三:服务端风控究竟在检测什么
4.1 轨迹时序特征的校验维度
很多人模拟轨迹时只看轮廓像不像,忽略了一个关键问题:服务端风控不是看一眼轨迹曲线就放行的,它会从时序数据里提取几十个特征做综合打分。我在分析南航验证交互时,重点梳理了服务端通常会关注的几个维度。
第一个维度是速度的连续性和平滑度。真人拖动时人体肌肉受惯性约束,速度变化是平滑过渡的,不会出现"从 0 瞬间跳到最大速度"的突变。对速度序列做一阶差分,人机差异非常显著。
第二个维度是加速度分布特征。真人在拖动开始阶段加速度较大,接近目标时减速并且加速度为负,整个过程加速度曲线有一个明显的正负交替。而很多简单轨迹生成器只控制速度,机械累加位移,加速度分布和真人差异很大。
第三个维度是轨迹的贝塞尔拟合误差。这是比较高级的特征:风控系统会把轨迹点序列拟合成贝塞尔曲线,然后计算原始点与拟合曲线的偏差。真人手部操作有随机波动,偏差分布比较自然;机器生成的轨迹往往过于贴合某种数学曲线,拟合误差过小,反而暴露。
这些维度告诉我们的核心结论是:模拟轨迹不能只追求位移准确,更要关注时序特征的统计分布。这也是为什么数据驱动的轨迹方案比手工调参的方案效果更好——先从真实用户操作中采集大量轨迹样本,统计特征分布,再按分布规律生成新轨迹,这种方案被识别出来的概率会显著下降。
4.2 环境指纹与行为上下文
轨迹本身只是风控判定的一个维度,真正被标记的高危信号往往来自环境指纹和行为上下文,这是做滑块算法分析时容易忽视的一层。
环境指纹包括浏览器指纹、canvas 指纹、WebGL 渲染指纹、字体指纹、时间区、屏幕分辨率、语言设置等。自动化工具如果用了默认配置,这些指纹之间会存在逻辑矛盾,比如浏览器语言是中文但时区是 UTC,这种组合在真人环境里几乎不会出现。风控系统通过对比指纹一致性,能快速识别自动化环境。
行为上下文指的是这次滑块验证之前的用户行为序列。真人登录南航 App,通常会从打开网页或 App 开始,经过页面浏览、输入账号密码等一系列行为,才会触发滑块验证。而自动化脚本往往直接请求验证接口,之前没有任何页面行为历史。这种"无上下文行为"本身就是极高危信号。
所以在做自动化测试或安全评估时,要意识到一个事实:即使轨迹模拟得完美,环境指纹和行为上下文仍然可能暴露自动化身份。这也是很多脚本通过率上不去的深层原因。
4.3 理解风控的最终目的:防御视角
分析到这里,有必要把视角转回防御端:理解这些检测算法不是为了绕过这套体系,而是反过来帮助我们设计更稳固的风控策略。
我在做安全评估时,会拿滑块验证做攻防演练,按三个层次检验验证码强度:第一层看缺口识别难度,如果图像处理几步就能精准定位缺口,说明图片干扰设计不够;第二层看轨迹模型识别率,如果随机生成的轨迹能拿到高通过率,说明行为模型需要强化;第三层看整体链路校验,如果绕过轨迹直接构造请求也能通过,说明服务端缺少轨迹一致性校验。
这种攻防视角对整个系统安全是有价值的。对风控团队来说,只有站在攻击者角度了解算法是怎么拆解验证码的,才能针对性地加固:比如增加干扰线、动态调整缺口位置、增加轨迹特征维度。对自动化测试团队来说,理解这些机制能帮助搭建更稳定的测试方案,避免因验证码误判导致测试随机失败。
5. 工程化落地与常见问题实录
5.1 自动化测试中的落地姿势
把滑块算法分析应用到 UI 自动化测试,通常要串起一整条链路,不只是调用一个缺口识别函数。我搭的自动化框架里,处理滑块验证的流程是:
- 等待验证码 iframe 出现,确认验证码类型;
- 获取背景图和滑块图地址,下载到本地;
- 按第 2 节的算法定位缺口坐标;
- 生成拟人轨迹,按轨迹模拟鼠标拖动;
- 等待验证结果,成功则继续业务操作,失败则刷新重试。
这里有一个工程化细节值得注意:必须处理验证码 iframe 的切换。南航的滑块验证码嵌在 iframe 里,Selenium 或 Playwright 操作前要先切换上下文,否则元素定位会一直失败。很多同学在环境上浪费大量时间,最后发现是 iframe 切换的问题。
另一个细节是等待策略。滑块图片是异步加载的,如果在图片资源还没加载完成时就开始识别,拿到的图片可能是空白或半加载状态。我通常用显式等待,等到 canvas 元素出现且图片高度大于某个阈值,再进入识别流程。
5.2 高频问题排查表
实操过程中遇到的坑,我整理成了一张排查表,遇到问题可以直接对照。
| 症状 | 可能原因 | 排查与解决方向 |
|---|---|---|
| 缺口定位明显偏左或偏右 | 滑块图有透明留白,坐标换算没做偏移修正 | 核对前端代码中滑块容器和背景图的定位关系,修正偏移常量 |
| 识别结果不稳定,成功率忽高忽低 | 背景图纹理复杂,Canny 阈值固定导致误检 | 换成动态阈值,对识别结果做多帧验证,置信度低时重试 |
| 轨迹看起来很像,但提交总是失败 | 落点误差为 0,或轨迹时间总长不合理 | 给终点加随机误差,检查时长是否落在 0.8 到 1.6 秒区间 |
| 验证码频繁刷新 | 同 IP 或同一环境触发频率过高,被风控标记 | 控制并发和重试频率,必要时换干净的环境验证 |
| 鼠标拖动事件没有触发 | 自动化工具没有正确切换到 iframe | 检查上下文切换,确认 iframe 加载完成后再操作 |
这个表还可以继续扩展,但大部分问题归根结底是三类:坐标换算不准、轨迹不拟真、环境被标记。按这三个方向排查,能解决九成的失败问题。
5.3 几个容易被忽视的坑
最后分享三个我在实战里踩过、但很少被人提起的坑。
第一个是截图分辨率与显示缩放。Windows 系统如果设置 125% 或 150% 的显示缩放,自动化工具拿到的页面元素坐标和实际截图像素坐标会不一致,导致识别出的缺口位置映射到鼠标移动坐标时整体偏移。解决方法是先统一坐标系:要么全部在截图坐标系里计算,再按缩放比例换算成屏幕坐标,要么直接用 CDP 的 DeviceMetricsOverride 覆盖缩放。
第二个是滑块拖动时不能只移动 x 轴。我之前写过一个简化版本,轨迹生成完美,但 y 轴完全不动,是条纯水平线。真人的手在水平拖动时,y 轴方向必然有 4 到 6 个像素的自然浮动。纯水平线在风控模型里是极度可疑的特征。
第三个是重试会累积风险。很多脚本的默认策略是失败就立刻重试,连续失败几十次也在短时间内完成。这在风控系统看来,是典型的自动化特征。更合理的做法是:失败后随机等待 3 到 8 秒再重试,连续失败超过 5 次就停止,等待一段时间或换环境后再继续。
6. 写在最后的实操体会
整套滑块算法分析做下来,我最深的体会是:滑块验证的本质是一场行为特征的概率博弈,而不是一场图像识别的精确对抗。缺口识别哪怕误差两三个像素,轨迹模拟得当照样能过;反过来,缺口识别再完美,轨迹和环境的任何一个细节暴露自动化特征,照样被拦截。
如果让我给刚开始接触这块的同行一个建议,那就是别上来就钻图像算法的牛角尖。先把整条链路跑通,把缺口识别精度控制在 5 到 10 像素内,把轨迹模拟做到"速度曲线符合物理直觉、落点有自然误差、时间总长在合理范围",这时候大概率已经能解决日常自动化测试的需求了。之后再根据实际被拦截的特征反馈,回头优化对应模块,这样走一圈后,体验会扎实很多。
再分享一个小技巧:调试轨迹模拟时,可以先在浏览器控制台里手动拖几次滑块,把 console 里打印的轨迹点记录下来,和自己的生成结果做对比。多对比几组真实轨迹和生成轨迹的差异,比看十篇理论分析都管用。真实数据永远是最标准的参照物。