滑块验证完整实现教程(前端 + 后端 + 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验证);

  • 依赖安装:

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

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

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

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="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>
    <div class="slider-container">
        <div class="slider-bg" id="sliderBg">
            <div class="slider-gap" id="sliderGap"></div>
        </div>
        <div class="slider-bar" id="sliderBar">
            <div class="slider-btn" id="sliderBtn">→</div>
        </div>
        <div class="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. 初始化:从后端获取验证图和缺口参数
        async function initVerify() {
            try {
                const res = await fetch('/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记录一次坐标和时间)
        function recordTrack(x, y) {
            track.push({
                time: Date.now(),
                x: x,
                y: y
            });
        }

        // 3. 滑动结束:提交轨迹到后端验证
        async function submitVerify() {
            if (track.length < 5) { // 轨迹过短,判定为机器
                tips.textContent = '验证失败:滑动轨迹异常';
                tips.style.color = '#f56c6c';
                resetSlider();
                return;
            }

            try {
                const res = await fetch('/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. 重置滑块和轨迹
        function resetSlider() {
            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调用):

python 复制代码
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')
if not os.path.exists(IMG_DIR):
    os.makedirs(IMG_DIR)

# -------------------------- 核心工具函数 --------------------------
# 1. 生成随机验证图(带缺口)
def generate_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 _ in range(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. 校验滑动轨迹(核心:区分真人/机器)
def check_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:
        return False, f'偏移量错误(目标{target_offset},实际{final_offset})'
    
    # 2. 轨迹长度校验:至少5个点(避免瞬间滑动)
    if len(track) < 5:
        return False, '轨迹过短'
    
    # 3. 滑动时长校验:0.5~3秒(真人滑动不会太快/太慢)
    total_time = track[-1]['time'] - track[0]['time']
    total_time_s = total_time / 1000
    if total_time_s < 0.5 or total_time_s > 3:
        return False, f'滑动时长异常({total_time_s:.2f}秒)'
    
    # 4. 速度波动校验:真人速度有波动,机器多匀速
    speeds = []
    for i in range(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 判定为匀速(机器)
    if len(speeds) < 3:
        return False, '轨迹点数不足'
    avg_speed = sum(speeds) / len(speeds)
    std_speed = (sum([(s - avg_speed)**2 for s in speeds]) / len(speeds)) ** 0.5
    if std_speed < 0.01:
        return False, '滑动速度匀速(疑似机器)'
    
    # 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:
        return False, 'Y轴无波动(疑似机器)'
    
    return True, '验证通过'

# 3. 生成验证token(供Nginx校验)
def generate_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'])
def init_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'])
def get_slider_img(img_name):
    img_path = os.path.join(IMG_DIR, img_name)
    if not 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'])
def verify_slider():
    try:
        data = request.get_json()
        verify_id = data.get('verifyId')
        track = data.get('track', [])
        final_offset = data.get('finalOffset', 0)
        
        # 1. 校验参数
        if not verify_id or not track or final_offset is None:
            return jsonify({'code': 400, 'msg': '参数缺失'}), 400
        
        # 2. 获取Redis中的验证参数
        verify_info_str = redis_client.get(f'slider_verify:{verify_id}')
        if not 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)
        if not 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'])
def check_token():
    # 从Cookie获取token
    token = request.cookies.get('slider_token')
    if not 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 '', 200
    else:
        return '', 401  # token无效

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=False)

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

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

nginx 复制代码
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. 部署优化

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

    nginx 复制代码
    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;
    }
  • 分布式部署:Redis使用集群,支持多实例后端服务;

  • 日志监控:记录验证失败日志,分析异常IP/设备,及时调整策略。

四、扩展适配

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、设备、频率),构建多层防御体系,平衡安全与用户体验。

相关推荐
网硕互联的小客服7 小时前
cdnfly节点到源服务器配置内网IP不生效怎么办
运维·服务器
albert-einstein7 小时前
Nginx越界读取缓存漏洞CVE-2017-7529(参考peiqi文库以及gpt)
gpt·nginx·缓存
kevin_水滴石穿7 小时前
Docker 健康检查(Healthcheck)
运维·docker·容器
yivifu7 小时前
Excel中Lookup函数实现临界点归入下一个等级的方法
java·前端·excel
Wiktok7 小时前
Tailwind CSS 自适应相关
前端·css·tailwindcss
LYFlied7 小时前
【一句话概括】Vue2 和 Vue3 的 diff 算法区别
前端·vue.js·算法·diff
元气满满-樱7 小时前
ansible-hoc 模块使用
运维·ansible
亮子AI7 小时前
Chrome 和 Edge 生成的 fingerprint 是一样的?
前端·chrome·edge
狼性书生7 小时前
uniapp实现的时间范围选择器组件
前端·uni-app·vue·组件·插件