滑块验证完整实现教程(前端 + 后端 + Nginx 集成)

滑块验证完整实现教程(前端+后端+Nginx集成)

滑块验证的核心逻辑是:前端渲染滑块+缺口背景图,采集用户滑动轨迹;后端校验轨迹是否为真人行为(非机器匀速滑动),验证通过后生成时效token;Nginx拦截业务请求,校验token有效性后放行。以下是可直接落地的完整方案,包含前端、后端、部署全流程。

一、核心原理

  1. 前端:生成随机的背景图+缺口,监听鼠标/触摸滑动事件,采集滑动轨迹(时间戳、X/Y坐标、速度、加速度),滑动完成后将轨迹和缺口偏移量传给后端。
  2. 后端:校验轨迹特征(如滑动时长、速度波动、是否匀速、缺口偏移匹配度),真人轨迹会有“先快后慢/轻微抖动”,机器轨迹多为“匀速直线”;验证通过则生成短期有效token。
  3. Nginx:拦截业务请求,校验请求头/Cookie中的验证token,有效则放行,无效则重定向到滑块验证页面。

二、完整实现步骤

步骤1:环境准备

  • 前端:无需框架,原生HTML+JS即可(也可适配Vue/React);
  • 后端:Python 3.8+ + Flask + Pillow(生成验证图) + Redis(存储token/验证参数);
  • Nginx:确保包含ngx_http_auth_request_module模块(默认编译,nginx -V验证);

依赖安装:

# 后端依赖 pip install flask pillow redis requests # Redis(本地/云服务器,用于存储验证参数和token)# 参考安装:https://redis.io/docs/getting-started/installation/

步骤2:前端实现(滑块渲染+轨迹采集)

新建slider.html,实现滑块渲染、滑动监听、轨迹采集和验证请求:

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>滑块验证</title><style>/* 滑块容器样式 */.slider-container{width: 320px;height: 160px;margin: 50px auto;border: 1px solid #e5e5e5;border-radius: 8px;padding: 10px;box-shadow: 0 2px 10px rgba(0,0,0,0.1);}.slider-bg{width: 100%;height: 120px;position: relative;border-radius: 4px;overflow: hidden;background: #f5f5f5;}.slider-gap{position: absolute;width: 40px;height: 40px;background: #fff;border: 1px solid #e5e5e5;box-shadow: 0 0 5px rgba(0,0,0,0.2);cursor: move;/* 缺口位置由后端返回,初始隐藏 */display: none;}.slider-bar{width: 100%;height: 30px;background: #f8f8f8;margin-top: 10px;border-radius: 15px;position: relative;cursor: pointer;}.slider-btn{width: 40px;height: 30px;background: #409eff;border-radius: 15px;position: absolute;top: 0;left: 0;box-shadow: 0 0 5px rgba(64,158,255,0.5);cursor: move;text-align: center;line-height: 30px;color: #fff;font-size: 12px;}.tips{text-align: center;margin-top: 10px;color: #666;font-size: 14px;}</style></head><body><divclass="slider-container"><divclass="slider-bg"id="sliderBg"><divclass="slider-gap"id="sliderGap"></div></div><divclass="slider-bar"id="sliderBar"><divclass="slider-btn"id="sliderBtn">→</div></div><divclass="tips"id="tips">请拖动滑块完成验证</div></div><script>// 核心变量const sliderBtn = document.getElementById('sliderBtn');const sliderBar = document.getElementById('sliderBar');const sliderGap = document.getElementById('sliderGap');const sliderBg = document.getElementById('sliderBg');const tips = document.getElementById('tips');let startX =0;// 滑动起始X坐标let isDragging =false;// 是否正在滑动let track =[];// 滑动轨迹:[{time: 时间戳, x: 坐标, y: 坐标}]let verifyId ='';// 本次验证的唯一ID(后端生成)let targetOffset =0;// 目标缺口偏移量(后端返回)// 1. 初始化:从后端获取验证图和缺口参数asyncfunctioninitVerify(){try{const res =awaitfetch('/api/slider/init');const data =await res.json();if(data.code ===200){ verifyId = data.data.verifyId; targetOffset = data.data.offset;// 目标偏移量(像素)// 设置背景图 sliderBg.style.background =`url(${data.data.bgImg}) no-repeat center/contain`;// 设置缺口位置 sliderGap.style.left =`${targetOffset}px`; sliderGap.style.top =`${data.data.top}px`; sliderGap.style.display ='block'; tips.textContent ='请拖动滑块完成验证'; tips.style.color ='#666';}}catch(e){ tips.textContent ='初始化失败,请刷新重试'; tips.style.color ='#f56c6c'; console.error('初始化失败:', e);}}// 2. 采集滑动轨迹(每10ms记录一次坐标和时间)functionrecordTrack(x, y){ track.push({ time: Date.now(), x: x, y: y });}// 3. 滑动结束:提交轨迹到后端验证asyncfunctionsubmitVerify(){if(track.length <5){// 轨迹过短,判定为机器 tips.textContent ='验证失败:滑动轨迹异常'; tips.style.color ='#f56c6c';resetSlider();return;}try{const res =awaitfetch('/api/slider/verify',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ verifyId: verifyId, track: track,// 滑动轨迹 finalOffset: sliderBtn.offsetLeft // 最终滑块偏移量})});const data =await res.json();if(data.code ===200){// 验证通过:设置token到Cookie(供Nginx校验) document.cookie =`slider_token=${data.data.token}; max-age=300; path=/`; tips.textContent ='验证通过!即将跳转...'; tips.style.color ='#67c23a';// 跳转到业务页面(或通知父页面)setTimeout(()=>{ window.location.href ='/';// 业务首页},1000);}else{ tips.textContent =`验证失败:${data.msg}`; tips.style.color ='#f56c6c';resetSlider();// 重新初始化验证setTimeout(initVerify,1000);}}catch(e){ tips.textContent ='验证请求失败,请重试'; tips.style.color ='#f56c6c';resetSlider(); console.error('验证提交失败:', e);}}// 4. 重置滑块和轨迹functionresetSlider(){ sliderBtn.style.left ='0px'; isDragging =false; track =[];}// 5. 绑定滑动事件// 鼠标按下/触摸开始 sliderBtn.addEventListener('mousedown',(e)=>{ isDragging =true; startX = e.clientX - sliderBtn.offsetLeft;// 开始记录轨迹recordTrack(sliderBtn.offsetLeft, e.clientY);// 每10ms持续记录轨迹 trackTimer =setInterval(()=>{if(isDragging){recordTrack(sliderBtn.offsetLeft, e.clientY);}},10);});// 触摸适配(移动端) sliderBtn.addEventListener('touchstart',(e)=>{ isDragging =true; startX = e.touches[0].clientX - sliderBtn.offsetLeft;recordTrack(sliderBtn.offsetLeft, e.touches[0].clientY); trackTimer =setInterval(()=>{if(isDragging){recordTrack(sliderBtn.offsetLeft, e.touches[0].clientY);}},10);});// 鼠标移动/触摸移动 document.addEventListener('mousemove',(e)=>{if(!isDragging)return;const newX = e.clientX - startX;// 限制滑块范围:0 ~ 滑块条宽度 - 滑块宽度const maxX = sliderBar.offsetWidth - sliderBtn.offsetWidth;const finalX = Math.max(0, Math.min(newX, maxX)); sliderBtn.style.left =`${finalX}px`;}); document.addEventListener('touchmove',(e)=>{if(!isDragging)return;const newX = e.touches[0].clientX - startX;const maxX = sliderBar.offsetWidth - sliderBtn.offsetWidth;const finalX = Math.max(0, Math.min(newX, maxX)); sliderBtn.style.left =`${finalX}px`;});// 鼠标松开/触摸结束 document.addEventListener('mouseup',()=>{if(!isDragging)return; isDragging =false;clearInterval(trackTimer);submitVerify();// 提交验证}); document.addEventListener('touchend',()=>{if(!isDragging)return; isDragging =false;clearInterval(trackTimer);submitVerify();});// 页面加载时初始化验证 window.onload = initVerify;</script></body></html>

步骤3:后端实现(验证图生成+轨迹校验)

新建slider_server.py,实现3个核心接口:初始化验证(生成背景图/缺口)、校验轨迹、验证token(供Nginx调用):

import os import uuid import random import time import json from PIL import Image, ImageDraw from flask import Flask, jsonify, request, send_file, make_response import redis import hashlib app = Flask(__name__)# Redis配置(存储验证参数和token,过期时间5分钟) redis_client = redis.Redis( host='127.0.0.1', port=6379, db=0, decode_responses=True, password=''# 如有密码请填写)# 配置:验证图存储路径(临时) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) IMG_DIR = os.path.join(BASE_DIR,'slider_imgs')ifnot os.path.exists(IMG_DIR): os.makedirs(IMG_DIR)# -------------------------- 核心工具函数 --------------------------# 1. 生成随机验证图(带缺口)defgenerate_slider_img():# 生成背景图(随机颜色+随机线条,模拟真实图片) width, height =300,100# 背景图尺寸 bg_img = Image.new('RGB',(width, height),(random.randint(230,255), random.randint(230,255), random.randint(230,255))) draw = ImageDraw.Draw(bg_img)# 画随机线条(增加干扰)for _ inrange(5): x1 = random.randint(0, width) y1 = random.randint(0, height) x2 = random.randint(0, width) y2 = random.randint(0, height) draw.line((x1, y1, x2, y2), fill=(random.randint(100,200), random.randint(100,200), random.randint(100,200)), width=2)# 生成缺口(随机位置:X轴 80~220,Y轴 30~70) gap_width, gap_height =40,40 gap_x = random.randint(80,220)# 缺口X偏移(目标偏移量) gap_y = random.randint(30,70)# 缺口Y偏移# 画缺口(白色矩形,模拟缺失) draw.rectangle((gap_x, gap_y, gap_x+gap_width, gap_y+gap_height), fill=(255,255,255))# 保存背景图 img_name =f'{uuid.uuid4()}.png' img_path = os.path.join(IMG_DIR, img_name) bg_img.save(img_path)return{'img_path': img_path,'img_name': img_name,'offset': gap_x,# 缺口X偏移量(目标值)'top': gap_y # 缺口Y偏移量}# 2. 校验滑动轨迹(核心:区分真人/机器)defcheck_track(track, final_offset, target_offset):""" :param track: 滑动轨迹列表 [{time, x, y}] :param final_offset: 用户最终滑动的偏移量 :param target_offset: 目标缺口偏移量 :return: (是否通过, 失败原因) """# 1. 偏移量校验:误差±5像素内 offset_error =abs(final_offset - target_offset)if offset_error >5:returnFalse,f'偏移量错误(目标{target_offset},实际{final_offset})'# 2. 轨迹长度校验:至少5个点(避免瞬间滑动)iflen(track)<5:returnFalse,'轨迹过短'# 3. 滑动时长校验:0.5~3秒(真人滑动不会太快/太慢) total_time = track[-1]['time']- track[0]['time'] total_time_s = total_time /1000if total_time_s <0.5or total_time_s >3:returnFalse,f'滑动时长异常({total_time_s:.2f}秒)'# 4. 速度波动校验:真人速度有波动,机器多匀速 speeds =[]for i inrange(1,len(track)): time_diff = track[i]['time']- track[i-1]['time'] x_diff = track[i]['x']- track[i-1]['x']if time_diff ==0:continue speed = x_diff / time_diff # 像素/毫秒 speeds.append(speed)# 计算速度标准差(波动值):<0.01 判定为匀速(机器)iflen(speeds)<3:returnFalse,'轨迹点数不足' avg_speed =sum(speeds)/len(speeds) std_speed =(sum([(s - avg_speed)**2for s in speeds])/len(speeds))**0.5if std_speed <0.01:returnFalse,'滑动速度匀速(疑似机器)'# 5. 轨迹Y轴校验:真人滑动Y轴有轻微波动,机器Y轴固定 y_values =[p['y']for p in track] y_max =max(y_values) y_min =min(y_values)if y_max - y_min <2:returnFalse,'Y轴无波动(疑似机器)'returnTrue,'验证通过'# 3. 生成验证token(供Nginx校验)defgenerate_token(verify_id): token = hashlib.md5(f'{verify_id}_{int(time.time())}_slider_secret'.encode()).hexdigest()# 存储token到Redis,过期5分钟 redis_client.setex(f'slider_token:{token}',300,'valid')return token # -------------------------- 接口实现 --------------------------# 1. 初始化验证接口(生成背景图+缺口参数)@app.route('/api/slider/init', methods=['GET'])definit_slider():try:# 生成验证图 img_info = generate_slider_img()# 生成唯一验证ID verify_id =str(uuid.uuid4())# 存储验证参数到Redis(过期5分钟) redis_client.setex(f'slider_verify:{verify_id}',300, json.dumps({'offset': img_info['offset'],'top': img_info['top'],'img_name': img_info['img_name']}))# 返回结果(图片路径为访问路径)return jsonify({'code':200,'msg':'初始化成功','data':{'verifyId': verify_id,'offset': img_info['offset'],'top': img_info['top'],'bgImg':f'/api/slider/img/{img_info["img_name"]}'# 图片访问接口}})except Exception as e:return jsonify({'code':500,'msg':f'初始化失败:{str(e)}'}),500# 2. 验证图片访问接口@app.route('/api/slider/img/<img_name>', methods=['GET'])defget_slider_img(img_name): img_path = os.path.join(IMG_DIR, img_name)ifnot os.path.exists(img_path):return jsonify({'code':404,'msg':'图片不存在'}),404# 返回图片,并设置缓存(短期) response = make_response(send_file(img_path, mimetype='image/png')) response.headers['Cache-Control']='max-age=300'return response # 3. 滑块验证接口(校验轨迹)@app.route('/api/slider/verify', methods=['POST'])defverify_slider():try: data = request.get_json() verify_id = data.get('verifyId') track = data.get('track',[]) final_offset = data.get('finalOffset',0)# 1. 校验参数ifnot verify_id ornot track or final_offset isNone:return jsonify({'code':400,'msg':'参数缺失'}),400# 2. 获取Redis中的验证参数 verify_info_str = redis_client.get(f'slider_verify:{verify_id}')ifnot verify_info_str:return jsonify({'code':400,'msg':'验证已过期,请刷新'}),400 verify_info = json.loads(verify_info_str) target_offset = verify_info['offset']# 3. 校验轨迹 is_pass, msg = check_track(track, final_offset, target_offset)ifnot is_pass:return jsonify({'code':403,'msg': msg}),403# 4. 验证通过:生成token,删除验证参数(防止复用) token = generate_token(verify_id) redis_client.delete(f'slider_verify:{verify_id}')return jsonify({'code':200,'msg':'验证通过','data':{'token': token}})except Exception as e:return jsonify({'code':500,'msg':f'验证失败:{str(e)}'}),500# 4. Nginx校验token接口(内部调用)@app.route('/api/slider/check_token', methods=['GET'])defcheck_token():# 从Cookie获取token token = request.cookies.get('slider_token')ifnot token:return'',401# 无token,验证失败# 校验token是否有效 is_valid = redis_client.get(f'slider_token:{token}')if is_valid =='valid':# 验证通过,删除token(防止复用) redis_client.delete(f'slider_token:{token}')return'',200else:return'',401# token无效if __name__ =='__main__': app.run(host='0.0.0.0', port=5001, debug=False)

步骤4:Nginx配置(拦截请求+校验token)

修改nginx.conf,实现“拦截业务请求→校验滑块token→放行/重定向”:

http { include mime.types; default_type application/octet-stream; sendfile on; keepalive_timeout 65; # 后端滑块验证服务 upstream slider_server { server 127.0.0.1:5001; } server { listen 80; server_name localhost; # 业务根路径(需要验证的路径) location / { # 1. 校验滑块token(内部子请求) auth_request /api/slider/check_token; # 2. token无效/缺失 → 重定向到滑块验证页面 error_page 401 = @redirect_slider; # 3. token有效 → 放行到业务服务(替换为你的实际业务地址) proxy_pass http://127.0.0.1:8080; # 你的业务服务地址 } # 重定向到滑块验证页面 location @redirect_slider { rewrite ^/(.*)$ /slider.html permanent; } # 滑块验证相关接口 → 转发到后端服务 location /api/slider/ { proxy_pass http://slider_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 滑块验证页面(静态文件) location /slider.html { root /path/to/your/static/files; # 替换为slider.html所在目录 expires 0; # 禁止缓存 } # Nginx内部调用的token校验接口(禁止外部访问) location = /api/slider/check_token { internal; proxy_pass http://slider_server; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } } } 

步骤5:启动与测试

  1. 启动Redisredis-server(确保端口6379);
  2. 启动后端服务python slider_server.py
  3. 放置前端文件:将slider.html放到Nginx配置的静态文件目录;
  4. 重启Nginxnginx -s reload
  5. 测试:访问http://localhost → 自动重定向到滑块验证页面 → 拖动滑块完成验证 → 验证通过后跳转到业务页面。

三、防破解优化(关键!避免被机器破解)

1. 前端优化

  • 轨迹加密:将轨迹数据用AES加密后传输(避免抓包篡改);
  • 混淆JS:对滑动事件的JS代码进行混淆(防止逆向分析);
  • 禁用模拟:检测是否为模拟器/自动化工具(如检测webdriver标识);
  • 动态样式:滑块样式随机变化(颜色、大小、形状),避免固定模板。

2. 后端优化

  • 动态阈值:根据IP/设备调整校验阈值(如高频验证的IP提高校验严格度);
  • 防重放:验证ID仅能使用一次,校验后立即删除;
  • 图片增强:验证图加入随机水印、扭曲、噪点(防止图像识别破解);
  • 风控结合:结合IP黑名单、设备指纹(如浏览器指纹)、访问频率限制。

3. 部署优化

  • 分布式部署:Redis使用集群,支持多实例后端服务;
  • 日志监控:记录验证失败日志,分析异常IP/设备,及时调整策略。

频率限制:Nginx配置limit_req_module限制验证接口请求频率(如每秒1次);

limit_req_zone $binary_remote_addr zone=slider:10m rate=1r/s; location /api/slider/ { limit_req zone=slider burst=2 nodelay; proxy_pass http://slider_server; } 

四、扩展适配

1. 移动端适配

  • 前端已兼容touch事件,只需调整样式适配移动端屏幕;
  • 优化滑块大小(如宽度280px,适配手机屏幕)。

2. 集成第三方滑块(简化开发)

如果不想自研,可直接集成成熟的第三方滑块:

  • 极验(GEETEST):https://www.geetest.com/ (文档完善,支持私有化部署);
  • 顶象:https://www.dingxiang-inc.com/ (风控能力强,适合高安全场景);
  • 集成方式:替换前端滑块代码为第三方SDK,后端调用第三方校验接口即可。

五、常见问题排查

  1. 验证图无法显示:检查IMG_DIR路径是否正确,Nginx是否有权限访问图片目录;
  2. 轨迹校验失败:调整check_track函数的阈值(如速度标准差、滑动时长);
  3. Redis连接失败:检查Redis地址、端口、密码是否正确,确保Redis服务运行;
  4. Nginx重定向循环:确保/api/slider/接口不被auth_request拦截(Nginx配置中排除)。

总结

滑块验证的核心是轨迹特征校验(区分真人/机器),而非单纯的偏移量匹配。自研方案适合中小场景,若追求更高安全性/更低开发成本,建议集成极验、顶象等第三方滑块服务。实际部署时,需结合风控策略(IP、设备、频率),构建多层防御体系,平衡安全与用户体验。

Read more

Spring Boot携手Leaflet,点亮省级旅游口号WebGIS可视化之路

Spring Boot携手Leaflet,点亮省级旅游口号WebGIS可视化之路

目录 前言 一、旅游口号信息管理 1、写在前面的 2、空间属性关联 二、SpringBoot后台实现 1、系统调用时序图 2、Mapper数据查询实现 3、控制层接口实现 三、Leaflet集成实现WebGIS 1、省级数据展示及可视化 2、东北三省旅游口号 3、长三角城市群口号 4、珠三角旅游口号 5、西北地区旅游口号 四、总结 前言         在当今数字化浪潮汹涌澎湃的时代,地理信息系统(GIS)技术正以前所未有的速度改变着我们对世界的认知与探索方式。它不仅为科学研究提供了强大的工具,更在旅游、城市规划、环境保护等诸多领域展现出巨大的应用潜力。而当我们将目光聚焦于旅游行业,一个充满活力与创新的领域,GIS技术的应用更是如鱼得水,为旅游体验的提升和旅        游管理的优化带来了全新的机遇。         省级旅游口号作为各地旅游宣传的重要名片,承载着地域文化的精髓与旅游资源的亮点,是吸引游客、塑造旅游品牌形象的关键要素。然而,传统的旅游口号宣传方式往往局限于文字、

DAMOYOLO-S代码实例:Python调用其Web API实现自动化目标检测流水线

DAMOYOLO-S代码实例:Python调用其Web API实现自动化目标检测流水线 你是不是也遇到过这样的场景?每天有成百上千张图片需要分析,手动上传、等待、下载结果,不仅效率低下,还容易出错。作为一名开发者,我经常需要处理大量的图像数据,寻找其中的特定目标——可能是监控视频中的异常行为,也可能是电商图片中的商品识别。 传统的目标检测方案要么需要复杂的本地部署,要么就是手动操作效率太低。直到我发现了DAMOYOLO-S这个高性能通用检测模型,特别是它提供的Web API服务,让我眼前一亮。今天,我就来分享如何用Python代码调用这个API,打造一个全自动的目标检测流水线。 1. DAMOYOLO-S:开箱即用的目标检测利器 1.1 什么是DAMOYOLO-S? DAMOYOLO-S是一个基于TinyNAS架构的高性能通用目标检测模型。简单来说,它就像一个“火眼金睛”,能够在一张图片中快速准确地找出各种物体,并告诉你它们是什么、在哪里。 这个模型有几个让我特别喜欢的特点: * 开箱即用:不需要自己训练模型,内置了COCO数据集的80个常见类别识别能力 * 部署简

旧安卓手机别扔!用KSWEB搭个人博客,搭配外网访问超香

旧安卓手机别扔!用KSWEB搭个人博客,搭配外网访问超香

KSWEB 作为安卓端轻量级 Web 服务器,核心功能是提供 PHP、MySQL 运行环境,能轻松部署 Typecho、WordPress 等博客系统,Termux 则可辅助管理内网穿透服务;这类工具特别适合预算有限的学生、个人博主,或是想折腾闲置设备的数码爱好者,优点也很突出 —— 对硬件要求极低,1GB 内存就能运行,旧款红米、华为畅享等机型都能适配,而且内置的运行环境无需手动配置,新手也能快速上手。 使用这套工具时也有不少需要注意的地方,比如手机要长期插电并连接稳定 Wi-Fi,否则服务容易中断;还要给 KSWEB 和 Termux 关闭电池优化、放开存储权限,我用小米手机测试时就因为没关后台限制,导致 Apache 服务频繁被系统杀掉,折腾了好一会儿才排查出问题;另外非 Root 机型也能使用,但部分文件权限操作会稍显繁琐。 不过仅靠 KSWEB 部署完博客后,只能在局域网内访问,这会带来很多不便:比如在家用电脑能连手机看博客,

Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:教室信息管理系统(前后端源码 + 数据库 sql 脚本)

Web 毕设篇-适合小白、初级入门练手的 Spring Boot Web 毕业设计项目:教室信息管理系统(前后端源码 + 数据库 sql 脚本)

🔥博客主页: 【小扳_-ZEEKLOG博客】 ❤感谢大家点赞👍收藏⭐评论✍         1.0 项目介绍         开发工具:IDEA、VScode         服务器:Tomcat, JDK 17         项目构建:maven         数据库:mysql 8.0 系统用户前台和管理后台两部分,项目采用前后端分离         前端技术:vue3 + elementUI         服务端技术:springboot + mybatis + redis + mysql         1.1 项目功能 后台功能:         1)登录、退出系统、首页         2)教室管理                 (1) 教室管理:添加、修改、删除、查询等功能。         3)教师管理