ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

滑块验证码前后端完整实现:从轨迹采集到防模拟登录

滑块验证码前后端完整实现:从轨迹采集到防模拟登录 滑块验证码在现在的前后端分离项目里几乎成了登录页标配但你搜一圈会发现绝大多数教程只讲了前端怎么拖顶多教你怎么把图片切两半后端到底怎么验、验什么、怎么防绕过很少有人写透。这篇就一次性把前后端完整逻辑讲清楚包括滑块轨迹怎么采集、后端怎么判定真人和机器、接口怎么设计才能扛住模拟登录全部基于我实际项目的写法可直接抄。1. 项目整体设计与思路拆解1.1 为什么滑块验证必须前后端配合很多人一开始做滑块验证习惯在前端用mousemove监听一下坐标拖到目标位置就提示校验通过后端接口不做任何校验。这种玩法说白了就是自欺欺人——我在Chrome控制台里直接调一下你的校验函数或者抓包改一下请求参数验证就过了。真正的滑块验证核心价值在于后端要有能力独立判断这次滑动到底是不是人滑的。前后端分离架构下前端负责交互体验和轨迹采集后端负责轨迹校验、验签和风控。两者配合的核心流程是这样的前端加载时向后端请求滑块验证码后端生成背景图、缺口图位置以及一个加密的ticket。前端完成滑动把滑动轨迹、耗时、偏移量等数据连同ticket一起提交给后端校验接口。后端根据ticket还原出真实的缺口位置对比前端提交的偏移量是否在容差范围内同时对轨迹数据做合法性分析最后返回校验结果。这个流程里关键点在于缺口位置绝对不能让前端来定。我见过有项目把targetX直接放在后端返回的JSON里传给前端前端拖到那个位置就行这种等于把答案印在试卷上了抓包的人一眼就能看到。正确做法是后端只返回背景图和缺口图的base64前端自己通过识别算法算出缺口位置或者后端返回的ticket里加密携带位置信息前端提交后由后端解密校验。1.2 技术选型与方案对比先说说主流的几种实现思路方便你根据项目情况做取舍。第一种是纯前端方案网上有很多开源的滑块组件比如vue-monoplasty-slide-verify。这种组件大概几百行代码前端拖一下、位置对了就通过。只适合那种对安全性没有任何要求的小工具页面登录、支付这类场景千万别用。第二种是前端组件后端验偏移量。后端返回ticket时把目标坐标加密进去前端提交ticket和偏移量后端解密后比对。这个方案能拦截掉90%的改参数直接过的脚本攻击因为ticket是一次性的且有时间限制。但对轨迹的校验基本没有如果攻击者用Playwright模拟拖拽还是能过。第三种是在第二种基础上增加轨迹行为分析。后端不仅比对坐标还会分析滑动轨迹的速度曲线、停顿、抖动、加速度等特征。真实人类滑动有明显的加速-减速过程和随机抖动机器的线性滑动一眼就能看出来。这套方案就是本文要写的核心也是目前工业界最常见的企业级做法——不需要复杂算法用统计特征就能挡掉大多数自动化脚本。技术栈方面前端我用的Vue 3 TypeScript后端Spring Boot。你如果用Vue 2或者其他后端语言核心逻辑一样直接把思路迁移过去就行。2. 前端组件实现2.1 滑块组件初始化与关键参数解析先定义一个Vue 3滑块组件完整的组件代码太长这里把关键部分拆开讲。组件初始化时需要向后端请求图片和ticket这个动作放在onMounted里完成。interface SliderInitData { backgroundImage: string; sliderImage: string; ticket: string; } interface SlideVerifyResult { success: boolean; message?: string; } // 初始化滑块验证获取背景图、滑块图和 ticket async function initSlider() { const res await fetch(/api/captcha/init, { method: POST, headers: { Content-Type: application/json } }); const data (await res.json()) as SliderInitData; backgroundImage.value data.backgroundImage; sliderImage.value data.sliderImage; ticket.value data.ticket; // 这里注意后端返回的图片是 base64data.backgroundImage 形如 // data:image/png;base64,iVBORw0KG... resetSlider(); }这个接口返回的数据格式有三个字段其中ticket是全流程的核心。ticket是后端生成的加密串里面包含了图片ID、缺口坐标、过期时间等信息。前端拿着这个ticket去拖动滑块提交校验。攻击者即便拿到了ticket也解不开里面的坐标信息因为他没有后端的密钥。缺口坐标不直接给前端那前端怎么判断拖到哪才算对齐两种做法一是前端本地做图像识别识别出背景图中的缺口位置二是后端把图片处理成带一个明显的缺口形状前端视觉上对齐即可提交的偏移量由后端容差校验。实际项目中更多采用的是纯视觉对齐方案——用户看到缺口自己拖过去对齐提交的X偏移量允许有正负5像素的误差。这是符合人类自然操作的反而太精确的偏移值得怀疑。2.2 拖动事件与轨迹采集实现拖动逻辑是前端这里最关键的代码。这里出问题后端拿到的轨迹数据质量就没有保障。// 保存轨迹数据 let trackList: Array{ x: number; y: number; t: number } []; // 滑块开始拖动的处理 function onSliderDown(event: MouseEvent | TouchEvent) { isDragging.value true; const clientX getClientX(event); startX.value clientX; dragStartTime.value Date.now(); trackList []; // 记录起点作为轨迹的起始坐标 trackList.push({ x: 0, y: 0, t: 0 }); document.addEventListener(mousemove, onSliderMove); document.addEventListener(mouseup, onSliderUp); } // 滑块移动过程中的处理 function onSliderMove(event: MouseEvent | TouchEvent) { if (!isDragging.value) return; const clientX getClientX(event); // 计算当前滑块移动的距离 let offsetX clientX - startX.value; // 边界限制滑块不能拖出容器范围 const maxOffset containerWidth.value - sliderWidth.value; offsetX Math.max(0, Math.min(offsetX, maxOffset)); sliderOffset.value offsetX; // 将轨迹点插入数组记录相对位置和相对时间 const currentTime Date.now() - startTime.value; const x Math.round(offsetX); const y Math.round(event.clientY - startY.value); tid: trackList.push({ x, y, t: currentTime }); } // 松开鼠标结束拖动触发校验 async function onSliderUp() { document.removeEventListener(mousemove, onSliderMove); document.removeEventListener(mouseup, onSliderUp); isDragging.value false; const track trackList; const elapsed Date.now() - startTime.value; const result await verifyCaptcha(ticket.value, sliderOffset.value, elapsed, track); if (result.success) { emit(success, ticket.value); } else { // 校验失败重置滑块 resetSlider(); } }这里有两个容易踩坑的细节。第一轨迹采样的频率不要固定比如不要在每帧requestAnimationFrame里都记录而是在mousemove事件中自然采样。真实鼠标移动的触发频率是不均匀的这本身就是人类行为的一个特征。如果你用定时器均匀采样计算机生成轨迹时会特别完美反而是破绽。第二轨迹的数据结构要设计好我这里的t是相对时间毫秒x、y是相对滑块起始位置的坐标。这个数据结构后面后端做行为分析时要解析保持简单清晰很重要。另外一个小细节getClientX要同时兼容鼠标和触摸事件。移动端拖动滑块非常常见如果你的组件只监听了mousemove在手机上完全没法用。简单封装一个获取坐标的函数即可这里不再展开。2.3 前端怎样才能避免被模拟登录直接破解热词里有带滑块验证的登录页面如何模拟登录这句话基本反映了甲方或者安全测试人员的焦虑。但在实现滑块时前端能做的主要是增加破解成本真正兜底还得靠后端。前端能做的有这么几件事。第一不使用typepassword的明文校验结果——也就是说滑块校验通过后不要在前端存储一个verifiedtrue的布尔值这种标志位在调试工具里改一下就直接绕过了。正确做法是校验通过后由后端返回一个短期有效的verifyToken登录时连同用户名密码一起提交后端再次校验这个token的有效性。第二前端代码做一下简单的混淆加密。不是说要上多复杂的混淆工具但至少把ticket的字段名改得没有规律不要用ticket、offset这种一眼看懂的名字增加手动分析脚本的成本。第三提交数据不要用标准的JSON字段名可以用固定顺序的字符串拼接。比如提交sliderOffset | elapsed | track后端按照约定的顺序解析。这种方式能挡住一部分懒惰的攻击者——他们抓包一看不是JSON可能就直接放弃了。当然真正的安全不能只依赖让攻击者嫌烦但前端多设置几个障碍后端的压力会小很多。这是攻防对抗的基本原则。3. 后端校验逻辑设计与实现3.1 后端接口设计与Ticket生成机制后端部分我用的Spring Boot但思路完全通用。总共设计两个接口一个是初始化接口一个是校验接口。初始化接口负责生成验证码图片和ticket校验接口负责接收前端提交的数据并判断是否通过。先看ticket的生成。ticket本质上是个加密token里面要封装的字段有验证码图片的唯一ID、缺口的x坐标、y坐标一般不需要X轴偏移就够了、过期时间戳。我习惯用AES加密后做Base64编码密钥放在后端配置文件里前端无法获取。public class SliderCaptchaService { // 生成 ticket内部使用 AES 加密 public String generateTicket(String imageId, int targetX, int targetY) { // 过期时间2分钟 long expireTime System.currentTimeMillis() 2 * 60 * 1000; String plainText imageId | targetX | targetY | expireTime; return aesEncrypt(plainText, secretKey); } // 解析 ticket返回 SN 图片id, targetX, targetY, expireTime public String[] parseTicket(String ticket) { String plainText aesDecrypt(ticket, secretKey); // 校验过期时间 // 校验是否已使用过Redis 记录 return plainText.split(\\|); } }初始化接口的完整流程是生成一张带缺口的背景图、生成对应的滑块图片、把目标和图片ID写入ticket返回给前端。图片的生成可以用Java的BufferedImage在内存中绘制也可以用预先准备的多套图片随机选一套。生产环境建议用后者因为实时切割图片对服务器CPU占用不小而且每次生成不一致的干扰线也会增加破解成本。RestController RequestMapping(/api/captcha) public class CaptchaController { PostMapping(/init) public ResultInitVO init() { // 1. 从图片库中随机选取一张背景图和对应的缺口坐标 CaptchaImage img captchaService.randomImage(); // 2. 生成带缺口的背景图和滑块图这里省略具体图像处理 // 3. 生成 ticket String ticket captchaService.generateTicket(img.getId(), img.getX(), img.getY()); // 4. 返回数据 return Result.success(new InitVO(img.getBackgroundBase64(), img.getSliderBase64(), ticket)); } }3.2 轨迹合法性校验与核心判断逻辑校验接口是重头戏。前端提交过来的参数有4个ticket、xOffset最终偏移量、elapsed滑动总耗时、track轨迹数组。后端要做的事情分三步验ticket、验偏移量、验轨迹。验ticket的代码大致是这样的String[] info captchaService.parseTicket(ticket); if (info null) { return Result.fail(ticket无效); } String imageId info[0]; int targetX Integer.parseInt(info[1]); long expireTime Long.parseLong(info[3]); if (System.currentTimeMillis() expireTime) { return Result.fail(验证码已过期); } // 通过 Redis 判断是否已使用过 if (redisTemplate.hasKey(captcha:used: ticket)) { return Result.fail(验证码已使用); }验偏移量就是拿用户提交的xOffset和ticket里的targetX做差绝对值小于5个像素就算通过。不过这里有个细节第一次返回的图片缺口位置X和后续用户拖动的距离是基于同一个坐标系必须保证两者的零点一致。如果前端传上来的是滑块移动的像素值而后端生成图片时基于整个画布的X坐标两者会差一个滑块的初始偏移这个偏差会导致校验永远不通过。我在项目里统一约定前端提交的xOffset是滑块从左侧初始位置移动的水平像素量而后端targetX是缺口位置从画布最左侧的像素坐标。两者的差值需要减去滑块初始X值通常是0保持一致后再比较。最后是轨迹校验这块内容最有价值。简单说后端拿到轨迹后要判断它像不像人滑出来的。我总结了几个有效的判断特征public boolean validateTrack(ListTrackPoint track, int targetX, long elapsed) { if (track null || track.size() 10) { return false; // 轨迹点太少大概率是模拟的 } if (elapsed 800 || elapsed 10000) { return false; // 正常人滑动不会太快或太慢 } // 1. 检查轨迹点x坐标是否有增有减人类手部会有轻微回弹 boolean hasBacktrack false; for (int i 1; i track.size(); i) { if (track.get(i).getX() - track.get(i - 1).getX() 0) { hasBacktrack true; break; } } if (!hasBacktrack) { return false; // 完全没有回退太像机器了 } // 2. 检查是否有停顿和加速过程 // 计算相邻两个点的时间间隔如果所有间隔几乎相等则是匀速风险高 // 计算速度差最后一段速度明显快于中间段说明有加速过程 // 3. 检查轨迹总长度是否大于直线距离的1.2倍以上 // 人类滑动轨迹不可能完全直线 return true; }轨迹校验的道理想通了之后其实不需要特别复杂的数学模型。人类滑动的核心特征有三个有回弹、有加减速、轨迹不完全是直线。抓住这三个特征简单代码就能实现。反而用太复杂的模型容易误伤正常用户得不偿失。3.3 后端接口的防刷与限流策略滑块验证接口比登录接口更容易被刷原因是攻击者可以在真正登录之前一遍又一遍地调用初始化接口拿到ticket和图片然后离线自动化分析。所以后端一定要做限流。我用的是最简单的基于IP的限流结合Redis的INCR命令public boolean checkRateLimit(String ip) { // 每分钟同一个IP最多调用 init 接口 10 次 String key captcha:init: ip : timestampOfMinute; Long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, Duration.ofMinutes(1)); } return count 10; }校验接口的限流策略要更严格一些因为校验接口可以直接被用来撞库或者暴力破解。同一个IP、同一个ticket、同一个用户名的失败次数都要做限制。我在项目里实践下来的经验是校验接口限制为每分钟30次但同一个ticket最多只能校验3次——因为正常人拖一次可能没对齐会再拖一次但不会反复拖十几次都不成功那更像是脚本在测试。还有一个容易被忽略的点ticket使用过后要立刻标记失效这个用Redis的SETNX来做判断是否已存在同样的key。这里必须考虑并发场景——攻击者可能同时发送多个请求携带同一个ticket如果不用原子操作可能出现两个请求都验证通过的情况。用SETNX ticket:used:xxx 1保证只有一个请求能拿到成功标志。4. 前后端联调与问题排查4.1 跨域与请求参数传递的坑前后端分离项目里最烦的问题就是跨域。滑块验证接口和登录接口都会遇到。解决跨域后端统一的CORS配置就好但是要注意如果你前端用了withCredentials: true携带Cookie后端的allowedOrigins不能写*必须明确指定域名否则浏览器会直接拦截请求。还有一个细节滑块轨迹里的时间戳和耗时前端用Date.now()获取的是客户端时间。如果用户电脑系统时间不准比如比服务器慢了两分钟那么ticket的2分钟过期时间就会出问题。我建议后端校验时对时间做兼容处理ticket里的过期时间用服务器时间生成但前端提交的elapsed是相对时间只要前端不传绝对时间戳时间偏差不会影响校验。这也是为什么轨迹时间点用相对时间而不是绝对时间的原因之一。请求参数传递方面前端提交的数据量不大用JSON请求体就行。但要注意轨迹数组完整序列化后可能有几十个点如果前端为了图省事把轨迹数组只传了最后几个点后端基于样本不足会直接拒绝。这个可以在前端代码里加个保护轨迹点少于20个时不发起请求直接重置滑块并提示用户重新拖动。4.2 模拟登录场景的应对思路热词里反复出现模拟登录站在开发者的角度我更愿意把它理解为怎么防止我的滑块被模拟。真正对抗模拟登录滑块验证只是第一道防线后续还应该有第二道——登录接口的二次校验。我在项目里面的做法是滑块验证通过后后端下发一个verifyToken有效期5分钟且只能使用一次。真正的登录接口请求时需要携带这个token登录成功后立即作废。攻击者即使直接调滑块校验接口模拟通过了拿到的新token但他如果不走完整的登录流程这个token一样没用。而如果攻击者用Playwright一类的工具完整模拟浏览器操作那他相当于要模拟整个登录流程攻击成本高了一个量级。另外在登录接口层面可以对请求的User-Agent、Referer、Cookie的一致性、X-Requested-With做检查。这些常规手段虽然不能完全阻断模拟登录但能提高门槛。真正高级的攻击者可以通过配置绕过但你要记住安全的本质是成本你能让对方为了绕过你的机制付出超过收益的成本你就算赢了。4.3 常见问题速查表把我实际项目中遇到过的问题整理成一张表供你联调时快速定位。现象可能原因排查与解决校验接口一直返回偏移量错误前后端坐标系定义不一致在前端打印xOffset数值和后端解析出的targetX比对两者是否在同一量纲比如是否有滑块初始偏移没减掉滑块拖到正确位置仍然报失败图片和缺口不是一套初始化时把imageId关联到图片库里校验时用同一个imageId不要动态随机生成图片坐标刷新页面第一次请求init报超时跨域问题导致预检请求失败检查CORS配置是否支持OPTIONS方法以及allowedOrigins是否精确匹配轨迹校验通过率极低正常用户被拒判断条件太严把elapsed的下限放宽到500mstrack.size()的要求从20降到10hasBacktrack改为允许最多3个轨迹点无回退攻击者使用加密ticket仍然被暴力破解接口缺少次数限制给校验接口加上同一个用户的调用频率限制以及同一个ticket的使用次数限制滑块图片加载不出来后端返回base64过大浏览器内存问题压缩图片质量到70%或者改用图片URL而非base64由前端直接请求图片静态资源手机上拖动滑块不灵敏只监听了mousemove事件增加touchmove、touchstart、touchend事件监听注意e.preventDefault()防止页面滚动4.4 踩过的坑与调试技巧这个项目做得多了有几个坑至今印象很深。第一个坑是图片缓存问题。前端fetchPOST请求获取base64图片没有缓存问题但如果是通过img标签加载图片URL默认会有浏览器缓存。滑块图片必须每次都不一样所以后端返回图片响应头要带Cache-Control: no-store否则用户第二次拖的时候拿到的还是第一次的图片和缺口位置而ticket已经换新的了怎么拖都对不上。第二个坑是后端图像处理时缺口位置加了随机偏移。有的后端实现为了让缺口不那么容易被图像识别算法直接定位会对缺口再加一个干扰偏移。但如果这个偏移量没有同步到ticket里前端用户好不容易把滑块拖到视觉上的缺口位置了后端校验却说偏移量过大这种体验很差。我的建议是如果你要加干扰偏移一定要同步更新ticket里的坐标值。第三个坑是关于接口数据的日志安全性。后端打印日志时不要把ticket和完整轨迹打印出来打印了也要脱敏。我遇到过一次测试环境日志泄露ticket的问题因为ticket可以解密出缺口坐标攻击者拿到日志就能直接解出正确答案。这是一个很少被人关注但实际风险很高的细节。调试技巧方面建议前端写一个专门的控制台调试模式。当URL带?debug1参数时前端把采集到的轨迹数据、耗时、最终偏移量全部打印在控制台并且把后端返回的校验结果也原样打印出来。这样联调的时候你不用一遍遍去猜是哪个环节出了问题直接看数据流就能定位。5. 扩展把滑块验证用在前端面试和项目实战中这个项目做完了拿去当面试项目讲效果会非常好。热词里出现了前端面试题2026、前端面试八股文、前后端分离项目实战说明很多人关心这个话题。面试官问你怎么设计一个滑块验证码你可以从三个层面展示你的水平。第一层是功能层面前端滑块组件、拖动交互、后端接口校验这是绝大多数候选人能说出来的。第二层是安全性层面ticket加密机制、轨迹行为分析、一次性token、限流和防刷这些讲出来就能和普通候选人拉开差距。第三层是工程化层面组件复用设计、图片资源预加载、监控报警、日志脱敏、异常降级策略比如滑块服务挂了直接放行避免阻塞用户登录这些体现的是全局视野。我见过很多候选人讲这个项目时只停留在第一层很可惜。实际上滑块验证是前后端分离项目里最能体现全栈思维的模块你不需要真正实现一个工业级的风控系统但把思路讲通面试官会高看你一眼。另外如果你想把这个项目往深处扩展可以考虑几个方向一是引入机器学习识别轨迹用随机森林或者简单的一维卷积网络对轨迹数据做二分类二是做成通用的微服务通过接口给多个业务系统复用三是增加无感验证模式当风控评分较高时自动跳过滑块提升用户体验。这些都是后续可以动手尝试的功能每一步做出来都是实打实的项目亮点。
返回列表