ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

滑动验证码原理拆解:从轨迹采集到前后端风控的完整实现

滑动验证码原理拆解:从轨迹采集到前后端风控的完整实现 很多刚接触前端的朋友第一次接触滑动验证码时都会觉得它挺神奇一个滑块、一张带缺口的图手指一拖松手就过了有时候甚至不用点。可真让你说说“它到底是怎么判断我不是机器人的”大多数人只能说个大概——大概是看滑动轨迹像不像真人吧。这句话对了一半但离真相还很远。作为一个写过验证码、也被验证码虐过无数次的从业者我想用一段尽量简短但接近真实逻辑的代码把滑动验证码前端这套“测谎”机制从头到尾拆一遍。这篇文章既适合准备前端面试的人也适合那些想自己动手做个简易验证码系统、又怕做出来一眼被机器人破解的开发者。看完以后你会明白前端不负责“判断真相”负责的是“收集尽可能像人的证据”然后把证据交给后端去裁决。1. 滑动验证码的本质它不是让你“滑一下”而是让你“像人一样滑”1.1 为什么会出现滑动验证码验证码的进化史其实就是人类和自动化脚本的斗争史。最早是扭曲字符让 OCR 一时抓瞎后来字符验证码被破得差不多了出现了选图验证码、点选验证码再后来用户开始不耐烦产品方也发现验证码太影响转化率于是“滑动验证码”这种交互成本极低的方案开始流行。但很多产品经理和前端新人有个误解觉得滑动验证码只要把滑块拖到对应位置就算验证通过。如果只是这样那它跟“把拼图放进拼图槽”没有本质区别自动化脚本只需要识别缺口位置、算出偏移量然后用模拟鼠标事件拖过去就行——三五十行代码就能攻破。滑动验证码真正的设计核心不在“最终位置对不对”而在“滑动过程是不是像人”。机器可以精确地滑到目标点但它很难模拟出人类那种带有随机性、抖动、加减速的动作曲线。所以你可以理解为滑动验证码本质是一种行为测谎仪它考察的不是答案而是答题姿势。1.2 前端不判断真相只负责收集证据这是新手最容易搞错的地方。很多人以为滑块松手后前端本地代码里返回一个true或false验证就结束了。实际上如果有任何一方只靠前端本地判断那这个系统的安全级别几乎为零。真实流程里前端要做的事情有三件记录用户从触摸/按下到松开整个过程的轨迹点、时间戳、坐标偏移根据轨迹数据算出一个“人味评分”作为初步参考把轨迹数据、评分、验证码ID、设备信息等打包加密发给后端由后端做最终判定。后端则维护着一套更严格的风控引擎。它会根据单次行为特征、账号历史行为、IP 情况、设备指纹等信息综合打分。“前端判断”更多是提供一个候选特征集真正的裁决权永远在后端。这个分工是理解整个滑动验证码安全模型的钥匙。2. 前端第一关滑动过程中到底采集了什么数据2.1 最核心的三类数据一段滑动交互看起来就是一个手指从左往右滑。但前端每秒能采到几十次坐标点这里面藏着大量信息。归纳起来核心数据有三类第一类位移相关。包括滑块从起点到终点的横向偏移量、纵向偏移量以及整个轨迹在 x 轴和 y 轴上的分布。人类滑动通常伴随轻微的纵向抖动而机器模拟的轨迹往往是一条笔直的线。第二类时间相关。按下时间、移动中的每个采样点时间戳、松手时间总耗时、每两个采样点之间的间隔。人手滑动通常先慢后快再减速两端有明显的加减速过程机器脚本往往是匀速。第三类事件本身。例如滑动过程是否出现跳跃上一帧在 x100下一帧直接到 x300、是否缺少中间事件、事件顺序是否正常。真实的人类滑动不会有一帧跨射几十个像素的现象。这三类数据组合起来基本可以判断一个人是不是在“正常地用鼠标/手指操作”。2.2 一段真实的轨迹采集代码口说无凭我直接给一段简化但结构完整的前端轨迹采集代码。这个代码你放到一个普通 HTML 页面里给滑块绑定事件就能打印出真实的滑动轨迹const slider document.getElementById(slider); const startX 100; // 滑块初始位置 x也可从 getBoundingClientRect 获取 const track []; let startTime 0; let pressed false; slider.addEventListener(mousedown, e { pressed true; startTime Date.now(); track.length 0; track.push({ x: e.clientX, y: e.clientY, t: 0 }); }); slider.addEventListener(mousemove, e { if (!pressed) return; track.push({ x: Math.round(e.clientX - startX), y: Math.round(e.clientY), t: Date.now() - startTime }); }); slider.addEventListener(mouseup, e { if (!pressed) return; pressed false; const endOffset Math.round(e.clientX - startX); const duration Date.now() - startTime; // 把轨迹和基础指标组装成数据包 const packet { track: track, offset: endOffset, duration: duration, pointCount: track.length }; console.log(JSON.stringify(packet)); });这里track数组里每一项都记录了采样点的横向位置、纵向位置、相对时间。移动事件触发的频率很高基本就是一次完整的物理轨迹。你别小看这几个字段后面所有行为评分、机器学习特征本质上都是围绕这些底层坐标点做加工的。2.3 为什么必须看“轨迹”而不是只看终点如果只判断“滑块最终有没有滑到缺口位置”那攻击者只需要做一件事算出缺口坐标直接把滑块瞬移过去。这对程序来说毫无难度。但一旦要求“必须有一段从起点到终点的连续轨迹”攻击难度就上来了。因为轨迹意味着过程中每个采样点都会被记录而程序生成的轨迹点和人肉生成的轨迹点特征差异非常明显。你可以把这两者类比成手写签名和打印签名打印出来的字永远一模一样但人写的字每次都有细微差别。验证码系统要做的就是识别这种“细微差别”能看出它到底是哪里来的。3. 判断由机器人还是人算法怎么打分3.1 前置条件拼图位置与目标偏移要实现滑动验证码第一步当然是生成一张带缺口的背景图和一张拼图块。这里有个关键点目标缺口的横坐标由谁决定、何时决定。有些实现是在前端生成缺口位置这种设计对安全性很不友好因为攻击者可以通过阅读前端代码直接拿到目标偏移量。更好的做法是后端生成背景图和缺口坐标前端只知道“存在一个目标位置”但不知道具体是哪。前端甚至会在请求验证码时只拿到一张加密的背景图和拼图块缺口坐标隐藏在服务端。不过在最初的 1.0 版本验证码里很多团队为了省事会把目标偏移量直接返回给前端。现在主流的商用验证码通常不会这样做而是通过图片抠图或者服务端下发坐标来保证安全性。作为学习者你可以先理解两种方式的差别面试时能说出这层演进已经是加分项。3.2 核心算法轨迹行为评分示例现在进入正题我把“判断”逻辑写成一段可以看懂、甚至可以跑起来尝试的代码。为了便于理解我做了大量简化只保留几个最有代表性的特征function judgeHuman(track, offset, duration) { let score 0; // 1. 总时长 if (duration 300) { score 30; // 300ms 内完成整个滑动太像编程了 } else if (duration 5000) { score 10; // 拖太久也许是机器人磨蹭也许是人 } // 2. 轨迹点数量 if (track.length 6) { score 30; // 少于 6 个点说明跳跃太大极不正常 } // 3. 纵向抖动幅度 let maxYOffset 0; for (let i 1; i track.length; i) { const diff Math.abs(track[i].y - track[i - 1].y); if (diff maxYOffset) maxYOffset diff; } if (maxYOffset 2) { score 20; // 全程几乎没有纵向抖动很像直线生成 } if (maxYOffset 50) { score 10; // 抖得也太夸张了也可能是脚本加随机噪声过头 } // 4. 水平方向是否有回退 let backCount 0; for (let i 1; i track.length; i) { if (track[i].x track[i - 1].x) backCount; } if (backCount 0) { score 10; // 人类偶尔会回退一点一次都不回退有点可疑 } // 5. 最后一段是否明显减速 if (track.length 3) { const firstSeg track[1].t - track[0].t; const lastSeg track[track.length - 1].t - track[track.length - 2].t; if (lastSeg firstSeg) { score - 5; // 最后反而加速收手不太像常人手势 } } return score; }这段代码里分数越高说明“机器味”越浓。你可以去调整几个参数试试如果轨迹是一条绝对笔直的匀速直线那么maxYOffset几乎为 0backCount大概率也是 0一旦时间短、点数少分数会迅速飙到 60 以上直接被判定为机器人。真实的商用系统当然不会用这么粗糙的规则他们大多用机器学习模型输入就是成千上万个轨迹样本输出一个“人/机”信心分。但底层逻辑和我上面写的是一致的从轨迹中抽取特征再按权重综合打分。搞懂这段基础代码后面理解模型就是顺水推舟的事。3.3 为什么“太完美”反而被判定为机器人这是很多人第一次看验证码代码时最容易困惑的点明明我交代程序用很平滑的贝塞尔曲线生成轨迹怎么还是被识别出来了答案很简单因为“完美”本身就是非人的特征。人类的运动受肌肉、神经反应、设备摩擦力等因素影响天生带有噪声。如果你画一条真实的手指拖拽轨迹会发现它并非教科书式的平滑曲线而是在整体有序中夹杂着微小抖动。打个比方你让一个人用笔沿直线画一条线他会画得歪歪扭扭但让你用尺子比着画虽然画出的是直线一眼就能看出那不是手画的。验证码背后的模型本质上就是在分辨“带有人类噪声的轨迹”和“为了模拟人类而强行加入噪声的轨迹”。所以不要试图用匀速直线或者完美贝塞尔曲线去糊弄它那反而是最容易被一眼识破的做法。4. 前端只是入口签名、加密和后端二次校验4.1 前端防线能拦住谁一个残酷的事实是前端所有代码都暴露在浏览器里任何人打开开发者工具都能看到。所以凡是完全靠前端规则判断的方案都不具备真正的安全性。前端的价值在于防御“普通脚本”比如一个只会识图并模拟拖拽的入门级攻击者这一层足够拦下大部分。但专门做攻击的人可以绕过整个前端逻辑直接擦除前端判断把伪造好的轨迹数据丢给后端接口。所以指望前端方案输出一个“人/机”结论决定一切在安全领域是不成立的。这也是为什么现在的验证码服务都会花费巨大精力去建设服务端风控。4.2 一次完整的验证流程我们把一次完整的滑动验证串起来看第一步用户点击“获取验证码”按钮前端向后端请求验证码。第二步后端返回背景图、拼图图以及一个包含随机 token 和超时时间的凭证。前端在用户拖动滑块时开始采集轨迹。第三步用户松手后前端把轨迹、token、签名、时间戳、设备指纹等打包通过一个加密接口提交。第四步后端拿到数据包后首先验签、验证时间戳是否过期、验证 token 是否已被使用然后重新执行行为分析结合用户历史画像生成最终结果。第五步后端返回结果前端根据结果决定是否放行、刷新验证码或者干脆无感通过。这里有个容易忽略但很重要的点token 必须与轨迹数据绑定且只能使用一次。否则攻击者完全可以拿到一个合法 token不断重复使用同一个通过验证的轨迹包。4.3 后端到底在复核什么很多开发者以为后端就是信任前端算出来的那个分数这又是一个误区。后端复核的维度远比前端看到的更广轨迹本身重新分析滑动曲线、速度、加速度是否符合人类分布账号维度这个账号是否频繁请求验证码、是否有大量失败记录环境维度浏览器指纹、UserAgent、IP、Session 是否可疑业务维度用户当前访问是否在高风险场景比如注册、改密、批量拉取数据。所以即便前端轨迹数据伪造得完美只要账号环境和操作行为露出破绽后端照样会判定为风险。这也是为什么某些时候用户明明用真实鼠标操作却仍然被验出异常——多半是账号或网络环境出了问题而不是“滑法”有问题。5. 绕过它的人都在做什么安全对抗与升级思路5.1 几种常见的自动化突破方式先聊聊“敌方操作”。理解攻击手段才能知道防御要做什么。第一种是直接跳过后端校验如果前端只凭接口返回值决定是否放行攻击者截包改返回即可。第二种是模拟轨迹。用脚本生成一段带随机抖动的贝塞尔曲线再通过 CDPChrome DevTools Protocol直接模拟鼠标事件。这类攻击的难点是轨迹看起来要足够自然。第三种是图像识别通过分析像素颜色或者做边缘检测自动标出拼图缺口位置。这种方式与轨迹无关它解决的是“目标位置在哪”的问题。5.2 防御方因此做了哪些升级面对这些攻击验证码系统的迭代方向基本可以概括成三条线。第一条线是让轨迹模型越来越复杂。早期规则判断容易破解后来大家开始用机器学习模型训练数据源来自真实用户的轨迹模型能捕捉比“抖动、回退、时长”更细微的统计特征。第二条线是从单点验证走向环境验证。单纯看轨迹不够就叠加浏览器指纹、摄像头、麦克风权限、Canvas 渲染差异等指标。这也是为什么你在无痕模式或者新装环境里很容易被拦住。第三条线是“无感化”。现在很多大风控体系的验证码已经不像以前那样要求你每次都手动拖一遍。它会在后台静默评分只有判断为低风险才直接放行一旦判断为可疑才会弹出滑块作为二次确认。这也是为什么很多人感觉“现在验证码出现得少了”——它正在被做得越来越无感。我在实际项目里最喜欢讲的一句话是验证码是一个随时可以被绕过的点要做的是无限抬高绕过成本而不是追求绝对不可破解。5.3 面试中如何回答“滑动验证码原理”因为这算是前端高频面试题之一我顺手给一个容易记住的回答框架你可以按这个思路展开先讲流程请求验证码 - 采集轨迹 - 前端初步评分 - 加密提交 - 后端复核再讲轨迹特征坐标点、时间戳、偏移量、加减速、抖动、回退强调前后端分工前端只是采集证据后端才是最终裁决最后补充安全对抗为什么不能只靠前端判断、token 一次性、环境指纹、机器学习模型。这套回答结构能体现你既有代码基础又有安全视角比单纯背概念值钱得多。6. 自己实现或调试滑动验证码时踩过的坑6.1 只在前端判断的惨痛教训有次我图省事在一个低风险后台系统里自己写了套简易验证码前端判断完事后端只检查滑块有没有拖到指定位置。结果上线第二天就被人写了个脚本刷了过去。原因很简单攻击者根本不走我的前端他直接分析接口发现只要传successtrue就能通过。从此我给自己立了一条规矩任何验证码类功能前端可以“体验好”但后端必须做二次校验。否则你做出来的不是验证码是摆设。6.2 高频 touchmove 带来的性能问题滑动过程中mousemove和touchmove触发的频率非常高如果回调里做大量计算主线程会被拖卡用户就会觉得滑块“不跟手”。解决思路是节流或采样。比如用requestAnimationFrame配合采样每帧只记录一个点而不是每个原始事件都记。这样轨迹点数足够又不至于让页面卡顿let lastRecordTime 0; slider.addEventListener(mousemove, e { if (!pressed) return; const now performance.now(); if (now - lastRecordTime 16) return; // 约 60fps 采样 lastRecordTime now; track.push({ x: e.clientX - startX, y: e.clientY, t: now - startTime }); });另外鼠标和触屏设备的事件有差异触屏是touchstart/touchmove/touchend坐标要取e.touches[0].clientX。封装一层统一事件处理能省掉大量兼容性 bug。6.3 时间戳和坐标基础单位不要搞混还有一个很容易踩的坑坐标单位不统一。比如滑块位置用的是offsetX但页面元素可能经过缩放或滚动导致坐标基准错乱。建议统一使用getBoundingClientRect加clientX/clientY来计算偏移并做好负数裁剪。时间方面也是一样有人用Date.now()有人用performance.now()混用会导致时间轴出现跳变。前后端对接时还要约定好时间精度是用毫秒还是微秒否则后端做速度计算时数据全是乱的。这些细节虽然不起眼但在真实项目中调试起来非常费劲一开始就把数据协议定清楚能省很多事。说实话滑动验证码这个功能看起来简单研究透了却能牵出浏览器事件模型、行为分析、前后端协作、安全对抗一整条知识链。对我个人来说自己动手实现一遍比看十篇原理文章都管用。如果你是正在准备面试建议把文章里的代码亲手跑一遍再自己调几个参数试试理解速度和记忆深度会完全不一样。
返回列表