简化版滑块验证(仅 X 轴滑动 + 美化 UI)

简化版滑块验证(仅X轴滑动+美化UI)

按你的需求优化:无需缺口匹配,只需从左滑到最右侧,通过滑动轨迹(速度、时长、波动)判断人机,同时大幅美化UI样式(渐变背景、圆角设计、滑动动画)。核心逻辑(轨迹校验、Nginx拦截、Redis存储)保持不变。

一、核心优化点

  1. 操作简化:去掉缺口,滑块从左滑到最右侧即可完成验证,用户无需对准缺口;
  2. UI美化
    • 渐变背景+磨砂玻璃效果;
    • 滑块带阴影+图标,滑动时有动画反馈;
    • 验证成功/失败有明确的颜色和图标提示;
  3. 交互优化:滑动过程中显示进度,松手后有结果动画。

二、完整实现步骤

步骤1:环境准备(同之前)

  • 前端:原生HTML+JS(美化样式);

  • 后端:Python 3.8+ + Flask + Pillow + Redis;

  • 依赖安装(同之前):

    bash 复制代码
    pip install flask pillow redis requests

步骤2:前端实现(简化操作+美化UI)

新建simple_slider.html,实现纯X轴滑动、美化样式、轨迹采集:

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <!-- 移动端核心适配:视口设置(关键) -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black">
    <meta name="format-detection" content="telephone=no, email=no">
    <title>简易滑块验证</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
            font-family: -apple-system, BlinkMacSystemFont, 'Microsoft YaHei', sans-serif;
            /* 移动端触摸高亮清除 */
            -webkit-tap-highlight-color: transparent;
            tap-highlight-color: transparent;
        }
        body {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 15px; /* 移动端增加内边距,避免贴边 */
            /* 移动端滚动优化 */
            -webkit-overflow-scrolling: touch;
            overflow-scrolling: touch;
        }
        /* 滑块容器:自适应宽度+移动端适配 */
        .slider-container {
            width: 100%;
            max-width: 380px; /* 最大宽度限制 */
            min-width: 280px; /* 最小宽度,适配小屏手机 */
            background: rgba(255, 255, 255, 0.95);
            border-radius: 12px;
            padding: 20px 15px; /* 移动端减少内边距 */
            box-shadow: 0 8px 32px rgba(31, 38, 135, 0.2);
            backdrop-filter: blur(4px);
            -webkit-backdrop-filter: blur(4px); /* 兼容iOS */
            border: 1px solid rgba(255, 255, 255, 0.18);
        }
        .slider-title {
            font-size: 17px; /* 移动端字体适配 */
            color: #333;
            margin-bottom: 18px;
            text-align: center;
            font-weight: 600;
        }
        /* 修正:添加id="sliderWrap",并优化移动端滑动区域 */
        .slider-wrap {
            width: 100%;
            height: 55px; /* 移动端增加高度,方便手指操作 */
            background: linear-gradient(90deg, #f0f2f5 0%, #e8f4f8 100%);
            border-radius: 28px; /* 对应高度调整圆角 */
            position: relative;
            overflow: hidden;
            box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
            /* 移动端防止触摸选中 */
            user-select: none;
            -webkit-user-select: none;
        }
        /* 滑块按钮:适配手指操作,增大点击区域 */
        .slider-btn {
            width: 55px; /* 增大宽度,适配手指 */
            height: 55px; /* 增大高度 */
            background: linear-gradient(135deg, #4299e1 0%, #38b2ac 100%);
            border-radius: 50%;
            position: absolute;
            top: 0;
            left: 0;
            box-shadow: 0 4px 12px rgba(66, 153, 225, 0.3);
            cursor: pointer;
            display: flex;
            justify-content: center;
            align-items: center;
            color: white;
            font-size: 22px; /* 增大图标 */
            transition: all 0.2s ease;
            z-index: 2;
            /* 移动端触摸优化 */
            touch-action: pan-x; /* 只允许水平滑动 */
            -webkit-touch-callout: none;
        }
        .slider-btn:hover {
            box-shadow: 0 6px 16px rgba(66, 153, 225, 0.4);
            transform: scale(1.03); /* 移动端缩小缩放,避免超出容器 */
        }
        .slider-btn:active {
            transform: scale(0.97);
        }
        /* 滑动进度条 */
        .slider-progress {
            height: 100%;
            background: linear-gradient(90deg, #4299e1 0%, #38b2ac 100%);
            width: 0;
            transition: width 0.15s linear; /* 移动端加快过渡,更跟手 */
            z-index: 1;
        }
        /* 提示文字:移动端字体适配 */
        .slider-tips {
            text-align: center;
            margin-top: 12px;
            font-size: 14px;
            color: #666;
            transition: color 0.3s ease;
            line-height: 1.4; /* 移动端增加行高 */
        }
        .tips-success {
            color: #48bb78;
            font-weight: 500;
        }
        .tips-error {
            color: #f56565;
        }
        /* 加载动画:适配移动端大小 */
        .loading {
            display: inline-block;
            width: 14px;
            height: 14px;
            border: 2px solid rgba(255, 255, 255, 0.5);
            border-top-color: white;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-right: 6px;
        }
        @keyframes spin {
            to { transform: rotate(360deg); }
        }
        /* 小屏手机适配(宽度<320px) */
        @media (max-width: 320px) {
            .slider-container {
                padding: 15px 10px;
            }
            .slider-title {
                font-size: 16px;
                margin-bottom: 15px;
            }
            .slider-wrap {
                height: 50px;
            }
            .slider-btn {
                width: 50px;
                height: 50px;
                font-size: 20px;
            }
            .slider-tips {
                font-size: 13px;
            }
        }
        /* 大屏手机适配(宽度>400px) */
        @media (min-width: 400px) {
            .slider-container {
                padding: 25px 20px;
            }
            .slider-title {
                font-size: 18px;
            }
        }
    </style>
</head>
<body>
    <div class="slider-container">
        <h3 class="slider-title">拖动滑块完成验证</h3>
        <!-- 核心修正:添加id="sliderWrap" -->
        <div class="slider-wrap" id="sliderWrap">
            <div class="slider-progress" id="sliderProgress"></div>
            <div class="slider-btn" id="sliderBtn">→</div>
        </div>
        <div class="slider-tips" id="sliderTips">请按住滑块,从左滑到右</div>
    </div>

    <script>
        // 核心变量
        const sliderBtn = document.getElementById('sliderBtn');
        const sliderWrap = document.getElementById('sliderWrap');
        const sliderProgress = document.getElementById('sliderProgress');
        const sliderTips = document.getElementById('sliderTips');
        let isDragging = false;
        let startX = 0;
        let track = [];
        let verifyId = '';
        let maxOffset = 0;
        let trackTimer = null;

        // 初始化最大偏移量(适配窗口变化)
        function initMaxOffset() {
            maxOffset = sliderWrap.offsetWidth - sliderBtn.offsetWidth;
        }

        // 1. 初始化验证
        async function initVerify() {
            try {
                // 移动端优化:请求添加超时
                const controller = new AbortController();
                const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时

                const res = await fetch('/api/slider/simple/init', {
                    signal: controller.signal,
                    // 移动端缓存优化
                    cache: 'no-cache',
                    headers: {
                        'X-Requested-With': 'XMLHttpRequest'
                    }
                });
                clearTimeout(timeoutId);

                const data = await res.json();
                if (data.code === 200) {
                    verifyId = data.data.verifyId;
                    resetSlider();
                }
            } catch (e) {
                if (e.name !== 'AbortError') {
                    sliderTips.textContent = '网络异常,请检查后重试';
                    sliderTips.className = 'slider-tips tips-error';
                } else {
                    sliderTips.textContent = '请求超时,请重试';
                    sliderTips.className = 'slider-tips tips-error';
                }
                console.error('初始化失败:', e);
            }
        }

        // 2. 采集滑动轨迹
        function recordTrack(x, y) {
            track.push({
                time: Date.now(),
                x: x,
                y: y
            });
        }

        // 3. 提交验证
        async function submitVerify() {
            const finalOffset = sliderBtn.offsetLeft;
            // 移动端容错:允许稍小的偏移(因为手指操作精度低)
            if (finalOffset < maxOffset - 5) {
                sliderTips.textContent = '请滑动到最右侧';
                sliderTips.className = 'slider-tips tips-error';
                resetSlider();
                return;
            }

            if (track.length < 3) { // 移动端降低轨迹长度要求(手指滑动更快)
                sliderTips.textContent = '滑动轨迹异常,请重试';
                sliderTips.className = 'slider-tips tips-error';
                resetSlider();
                return;
            }

            sliderTips.innerHTML = '<span class="loading"></span>验证中...';
            sliderTips.className = 'slider-tips';

            try {
                const controller = new AbortController();
                const timeoutId = setTimeout(() => controller.abort(), 5000);

                const res = await fetch('/api/slider/simple/verify', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-Requested-With': 'XMLHttpRequest'
                    },
                    body: JSON.stringify({
                        verifyId: verifyId,
                        track: track,
                        finalOffset: finalOffset,
                        maxOffset: maxOffset
                    }),
                    signal: controller.signal,
                    cache: 'no-cache'
                });
                clearTimeout(timeoutId);

                const data = await res.json();
                if (data.code === 200) {
                    document.cookie = `slider_token=${data.data.token}; max-age=300; path=/; secure; SameSite=Lax`; // 移动端Cookie安全配置
                    sliderTips.innerHTML = '✅ 验证通过!即将跳转...';
                    sliderTips.className = 'slider-tips tips-success';
                    sliderBtn.innerHTML = '✓';
                    sliderBtn.style.background = 'linear-gradient(135deg, #48bb78 0%, #38a169 100%)';
                    setTimeout(() => {
                        window.location.href = '/';
                    }, 1500);
                } else {
                    sliderTips.innerHTML = `❌ ${data.msg}`;
                    sliderTips.className = 'slider-tips tips-error';
                    resetSlider();
                    setTimeout(initVerify, 1500);
                }
            } catch (e) {
                if (e.name !== 'AbortError') {
                    sliderTips.textContent = '验证失败,请重试';
                } else {
                    sliderTips.textContent = '验证超时,请重试';
                }
                sliderTips.className = 'slider-tips tips-error';
                resetSlider();
                console.error('验证失败:', e);
            }
        }

        // 4. 重置滑块
        function resetSlider() {
            sliderBtn.style.left = '0px';
            sliderProgress.style.width = '0%';
            sliderBtn.innerHTML = '→';
            sliderBtn.style.background = 'linear-gradient(135deg, #4299e1 0%, #38b2ac 100%)';
            isDragging = false;
            track = [];
            sliderTips.textContent = '请按住滑块,从左滑到右';
            sliderTips.className = 'slider-tips';
            // 清除定时器,防止内存泄漏
            if (trackTimer) clearInterval(trackTimer);
        }

        // 5. 绑定事件(优化移动端触摸)
        function bindEvents() {
            // 鼠标按下/触摸开始
            const startHandler = (e) => {
                e.preventDefault(); // 阻止默认行为(如页面滚动)
                isDragging = true;

                // 区分鼠标和触摸事件
                let clientX = e.clientX;
                let clientY = e.clientY;
                if (e.type === 'touchstart') {
                    clientX = e.touches[0].clientX;
                    clientY = e.touches[0].clientY;
                }

                startX = clientX - sliderBtn.offsetLeft;
                recordTrack(sliderBtn.offsetLeft, clientY);

                trackTimer = setInterval(() => {
                    if (isDragging) {
                        // 触摸时实时获取坐标(防止手指移动后坐标不变)
                        let x = sliderBtn.offsetLeft;
                        let y = clientY;
                        if (e.type === 'touchstart' && isDragging) {
                            const touch = document.querySelector('body').ontouchmove ? event.touches[0] : null;
                            if (touch) y = touch.clientY;
                        }
                        recordTrack(x, y);
                    }
                }, 10);
            };

            // 鼠标/触摸移动
            const moveHandler = (e) => {
                if (!isDragging) return;
                e.preventDefault(); // 阻止页面滚动

                let clientX = e.clientX;
                if (e.type === 'touchmove') {
                    clientX = e.touches[0].clientX;
                }

                let newX = clientX - startX;
                newX = Math.max(0, Math.min(newX, maxOffset));
                sliderBtn.style.left = `${newX}px`;
                sliderProgress.style.width = `${(newX / maxOffset) * 100}%`;
            };

            // 鼠标/触摸结束
            const endHandler = () => {
                if (!isDragging) return;
                isDragging = false;
                clearInterval(trackTimer);
                submitVerify();
            };

            // 绑定事件(移动端优先)
            // 触摸事件:添加passive优化性能
            sliderBtn.addEventListener('touchstart', startHandler, { passive: false });
            document.addEventListener('touchmove', moveHandler, { passive: false });
            document.addEventListener('touchend', endHandler);
            document.addEventListener('touchcancel', endHandler); // 处理触摸中断(如来电)

            // 鼠标事件
            sliderBtn.addEventListener('mousedown', startHandler);
            document.addEventListener('mousemove', moveHandler);
            document.addEventListener('mouseup', endHandler);
            document.addEventListener('mouseleave', endHandler); // 鼠标离开窗口时重置
        }

        // 初始化
        window.addEventListener('DOMContentLoaded', () => {
            initMaxOffset();
            initVerify();
            bindEvents();

            // 窗口大小变化时重新计算偏移量
            window.addEventListener('resize', () => {
                initMaxOffset();
                resetSlider();
            });

            // 移动端返回键处理
            window.addEventListener('popstate', () => {
                resetSlider();
            });
        });

        // 防止移动端页面卸载时的定时器泄漏
        window.addEventListener('beforeunload', () => {
            if (trackTimer) clearInterval(trackTimer);
        });
    </script>
</body>
</html>

步骤3:后端实现(简化逻辑+保持轨迹校验)

新建simple_slider_server.py,去掉缺口相关逻辑,保留核心轨迹校验:

python 复制代码
import os
import uuid
import random
import time
import json
from PIL import Image, ImageDraw
from flask import Flask, jsonify, request, make_response
import redis
import hashlib

app = Flask(__name__)
# Redis配置
redis_client = redis.Redis(
    host='127.0.0.1',
    port=6379,
    db=0,
    decode_responses=True,
    password=''  # 如有密码请填写
)

# -------------------------- 核心工具函数 --------------------------
# 1. 校验滑动轨迹(核心:真人vs机器)
def check_track(track, final_offset, max_offset):
    """
    :param track: 滑动轨迹列表 [{time, x, y}]
    :param final_offset: 用户最终滑动偏移量
    :param max_offset: 最大偏移量(滑到最右侧的值)
    :return: (是否通过, 失败原因)
    """
    # 1. 校验是否滑到最右侧(误差±3px)
    if final_offset < max_offset - 3:
        return False, '未滑动到最右侧'
    
    # 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)
    
    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_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轴无波动(疑似机器)'
    
    # 6. 滑动加速度校验:真人滑动先加速后减速,机器加速度固定
    accelerations = []
    for i in range(1, len(speeds)):
        acc = speeds[i] - speeds[i-1]
        accelerations.append(acc)
    if len(accelerations) > 0:
        acc_std = (sum([a**2 for a in accelerations]) / len(accelerations)) ** 0.5
        if acc_std < 0.005:
            return False, '加速度无变化(疑似机器)'
    
    return True, '验证通过'

# 2. 生成验证token(供Nginx校验)
def generate_token(verify_id):
    token = hashlib.md5(f'{verify_id}_{int(time.time())}_simple_slider_secret'.encode()).hexdigest()
    redis_client.setex(f'slider_token:{token}', 300, 'valid')
    return token

# -------------------------- 接口实现 --------------------------
# 1. 初始化验证接口(仅生成唯一ID,无需图片)
@app.route('/api/slider/simple/init', methods=['GET'])
def init_simple_slider():
    try:
        verify_id = str(uuid.uuid4())
        # 存储验证ID到Redis(过期5分钟,用于防重放)
        redis_client.setex(f'slider_verify:{verify_id}', 300, 'valid')
        return jsonify({
            'code': 200,
            'msg': '初始化成功',
            'data': {'verifyId': verify_id}
        })
    except Exception as e:
        return jsonify({'code': 500, 'msg': f'初始化失败:{str(e)}'}), 500

# 2. 滑块验证接口(校验轨迹)
@app.route('/api/slider/simple/verify', methods=['POST'])
def verify_simple_slider():
    try:
        data = request.get_json()
        verify_id = data.get('verifyId')
        track = data.get('track', [])
        final_offset = data.get('finalOffset', 0)
        max_offset = data.get('maxOffset', 0)
        
        # 1. 校验参数
        if not verify_id or not track or final_offset is None or max_offset == 0:
            return jsonify({'code': 400, 'msg': '参数缺失'}), 400
        
        # 2. 校验验证ID是否有效(防重放)
        if not redis_client.get(f'slider_verify:{verify_id}'):
            return jsonify({'code': 400, 'msg': '验证已过期,请重试'}), 400
        
        # 3. 校验轨迹
        is_pass, msg = check_track(track, final_offset, max_offset)
        if not is_pass:
            return jsonify({'code': 403, 'msg': msg}), 403
        
        # 4. 验证通过:生成token,删除验证ID(防止复用)
        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

# 3. Nginx校验token接口(同之前,可复用)
@app.route('/api/slider/check_token', methods=['GET'])
def check_token():
    token = request.cookies.get('slider_token')
    if not token:
        return '', 401
    is_valid = redis_client.get(f'slider_token:{token}')
    if is_valid == 'valid':
        redis_client.delete(f'slider_token:{token}')
        return '', 200
    else:
        return '', 401

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

步骤4:Nginx配置(仅修改静态文件路径)

修改nginx.conf,只需更新滑块页面的路径,其他逻辑不变:

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 / {
            auth_request /api/slider/check_token;
            error_page 401 = @redirect_slider;
            proxy_pass http://127.0.0.1:8080;  # 你的业务服务地址
        }

        # 重定向到简化版滑块页面
        location @redirect_slider {
            rewrite ^/(.*)$ /simple_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 /simple_slider.html {
            root /path/to/your/static/files;  # 替换为simple_slider.html所在目录
            expires 0;
        }

        # 内部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. 启动Redis:redis-server
  2. 启动后端服务:python simple_slider_server.py
  3. 放置前端文件:将simple_slider.html放到Nginx静态目录;
  4. 重启Nginx:nginx -s reload
  5. 测试:访问http://localhost → 重定向到美化后的滑块页面 → 从左滑到右 → 验证通过后跳转。

三、核心优化说明

1. 操作简化

  • 去掉缺口匹配,用户只需"从左滑到右",降低操作成本;
  • 滑动过程中显示进度条,用户明确知道滑动目标。

2. UI美化

  • 渐变背景+磨砂玻璃卡片,视觉更高级;
  • 滑块带阴影和图标,滑动时有缩放/颜色变化反馈;
  • 验证结果用图标+颜色区分,用户体验更清晰;
  • 响应式设计,适配手机和电脑。

3. 安全性保持

  • 保留核心轨迹校验规则(时长、速度波动、Y轴波动、加速度);
  • 验证ID防重放、token时效性控制;
  • 支持分布式部署(Redis存储)。

四、进一步优化建议

  1. 前端加密:将轨迹数据用AES加密后传输,防止抓包篡改;
  2. JS混淆:对滑动事件代码进行混淆,防止逆向分析;
  3. 设备指纹:结合浏览器指纹(如User-Agent、屏幕分辨率)增强校验;
  4. 频率限制 :Nginx配置limit_req限制验证接口请求频率,防止暴力破解。

如果需要进一步调整UI风格(如颜色、大小、动画),可以直接修改前端CSS部分,核心逻辑无需改动。

相关推荐
百锦再4 小时前
与AI沟通的正确方式——AI提示词:原理、策略与精通之道
android·java·开发语言·人工智能·python·ui·uni-app
Just_Paranoid5 小时前
【Android UI】Android Tint 用法指南
android·ui·tint·porterduff·colorfilter
reddingtons1 天前
PS 参考图像:线稿上色太慢?AI 3秒“喂”出精细厚涂
前端·人工智能·游戏·ui·aigc·游戏策划·游戏美术
喜欢踢足球的老罗1 天前
Swagger UI 自定义请求头:从用户配置到请求注入的完整流程解析
ui
Just_Paranoid1 天前
【Android UI】Android Drawable XML 标签解析
android·ui·vector·drawable·shape·selector
jinxinyuuuus1 天前
Wallpaper Generator:前端性能优化、UI状态管理与实时渲染的用户体验
前端·ui·性能优化
Just_Paranoid1 天前
【Android UI】Android 创建渐变背景 Drawable
android·ui·drawable·shape·gradient
Aevget1 天前
DevExpress WPF中文教程:Data Grid - 如何绑定到有限制的自定义服务(一)?
ui·.net·wpf·devexpress·ui开发·wpf界面控件
yuegu7772 天前
DevUI的Quadrant Diagram四象限图组件功能解析和使用指南
ui·前端框架