Files
chunyu_project/docs/slider-captcha-usage.md
2026-08-05 23:59:15 +08:00

15 KiB
Raw Permalink Blame History

滑块验证码系统使用指南

概述

滑块验证码是一种基于用户交互行为的验证机制,通过让用户拖动滑块到指定位置来判断操作是否为真人发起。本系统包含三个核心部分:

  1. 核心工具模块 (utils/slider_captcha.py) - 验证码生成、位置验证、轨迹分析
  2. API接口层 (user/views/slider_captcha.py) - 提供RESTful API供前端调用
  3. 前端交互组件 (static/js/slider-captcha.js) - 用户拖拽交互界面

系统架构

┌─────────────────┐     GET /slider-captcha/generate/     ┌─────────────────┐
│   前端浏览器     │ ─────────────────────────────────────> │   Django后端    │
│  (HTML/Canvas)  │                                         │   (APIView)     │
│                 │   返回: {captcha_key, bg_image,        │                 │
│                 │            slider_image, y_position}   │                 │
└────────┬────────┘ <──────────────────────────────────────┴────────┬────────┘
         │                                                           │
         │  用户拖拽滑块                                             │  Redis缓存
         │                                                           │  captcha_key →
         │  POST /slider-captcha/verify/                            │  {x_position,
         │  {captcha_key, x_position, trajectory: [...]}            │   timestamp}
         │                                                           │
         v                                                           v
┌─────────────────┐ ─────────────────────────────────────> ┌─────────────────┐
│   前端浏览器     │   验证结果: {verified: true/false}       │   Django后端    │
│  (JS组件)       │                                         │  (utils模块)    │
│                 │ <────────────────────────────────────── │   位置验证      │
│                 │                                           │   轨迹分析      │
└─────────────────┘                                           └─────────────────┘

API 接口文档

1. 生成滑块验证码

请求

GET /user/slider-captcha/generate/
Content-Type: application/json

响应 (200 OK)

{
    "code": 0,
    "message": "success",
    "data": {
        "captcha_key": "550e8400-e29b-41d4-a716-446655440000",
        "bg_image": "data:image/png;base64,iVBORw0KGgoAAAANS...",
        "slider_image": "data:image/png;base64,iVBORw0KGgoAAAANS...",
        "y_position": 50
    }
}

字段说明:

  • captcha_key: 验证码唯一标识,用于后续验证请求。UUID格式
  • bg_image: 背景图片。base64编码的PNG格式,data URI
  • slider_image: 滑块图片。base64编码的PNG格式,需要用户拖动到背景图的凹槽位置
  • y_position: 滑块在背景图中的垂直位置(像素)

2. 验证滑块位置

请求

POST /user/slider-captcha/verify/
Content-Type: application/json

{
    "captcha_key": "550e8400-e29b-41d4-a716-446655440000",
    "x_position": 150,
    "trajectory": [
        {"x": 0, "y": 150, "t": 0},
        {"x": 50, "y": 152, "t": 100},
        {"x": 100, "y": 148, "t": 200},
        {"x": 150, "y": 150, "t": 300}
    ]
}

请求参数:

  • captcha_key (必填): 从generate接口获取的验证码标识
  • x_position (必填, integer): 用户拖动滑块到的最终水平位置(像素)
  • trajectory (可选, array): 拖拽过程轨迹点数组,用于机器人检测
    • 每个轨迹点包含 x (水平位置), y (垂直位置), t (时间戳,毫秒)

响应 (200 OK)

{
    "code": 0,
    "message": "success",
    "data": {
        "verified": true
    }
}

错误响应 (400 Bad Request)

{
    "code": 20022,
    "message": "缺少必要参数",
    "data": null
}

验证码过期错误响应:

{
    "code": 20021,
    "message": "验证码已过期,请重新获取",
    "data": null
}

后端验证逻辑详解

1. 位置验证

from utils.slider_captcha import verify_slider_captcha

# 验证滑块位置
result = verify_slider_captcha(captcha_key, x_position)
# result: True (位置在误差范围内) 或 False (位置错误)

机制:

  • 系统允许±5像素的误差容差(TOLERANCE = 5)
  • 正确位置存储在Redis中,key格式为slider_captcha_{captcha_key}
  • 验证成功后缓存立即删除(一次性使用)
  • 验证码有效期为5分钟(CAPTCHA_TIMEOUT = 300秒)

2. 轨迹分析(防机器人)

from utils.slider_captcha import verify_slider_trajectory

# 带轨迹验证
trajectory = [
    {'x': 0, 'y': 150, 't': 0},
    {'x': 50, 'y': 152, 't': 100},
    {'x': 100, 'y': 148, 't': 200},
    {'x': 150, 'y': 151, 't': 300},
]
result = verify_slider_trajectory(captcha_key, x_position, trajectory)
# result: True (轨迹符合人类操作特征) 或 False (轨迹可疑)

检测原理:

  1. 速度变化检测 - 人类操作有自然的加速/减速,机器人操作速度恒定
  2. 微小停顿检测 - 人类操作过程中常有毫秒级的微小停顿
  3. 轨迹点数量 - 至少需要5个轨迹点(TRAJECTORY_MIN_POINTS = 5)
  4. 时间范围检查 - 总操作时间不超过10秒(TRAJECTORY_MAX_DURATION = 10000毫秒)

前端集成指南

1. 引入静态资源

在需要滑块验证码的页面中引入:

<link rel="stylesheet" href="{% static 'css/slider-captcha.css' %}">
<script src="{% static 'js/slider-captcha.js' %}"></script>

2. 添加HTML容器

<div id="slider-captcha-container"></div>

3. 初始化组件

const captcha = new SliderCaptcha({
    containerId: 'slider-captcha-container',
    apiUrl: '/user/slider-captcha/',
    onSuccess: function(captchaKey) {
        console.log('滑块验证成功:', captchaKey);
        // 验证成功后,隐藏滑块验证码,提交表单
        document.getElementById('captcha-key').value = captchaKey;
    },
    onError: function() {
        console.log('滑块验证失败');
        // 显示错误信息,1.5秒后自动刷新新验证码
    },
    onRefresh: function() {
        console.log('用户点击刷新');
    }
});

4. 在表单提交中使用

document.getElementById('login-form').addEventListener('submit', async function(e) {
    e.preventDefault();

    const formData = {
        account: document.getElementById('account').value,
        password: document.getElementById('password').value,
        slider_captcha_key: captcha.captchaKey,  // 从组件获取
        slider_captcha_x: captcha.finalXPosition
    };

    const response = await fetch('/user/login/', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(formData)
    });

    const data = await response.json();
    if (data.code === 0) {
        // 登录成功
        window.location.href = '/dashboard/';
    } else {
        // 显示错误,刷新验证码
        captcha.refresh();
    }
});

在后端视图中集成滑块验证

1. 在现有视图中添加滑块验证码保护

参考 user.py 中的 LoginView 实现:

from rest_framework.views import APIView
from rest_framework import status

from utils.captcha import check_captcha_required, record_failure, reset_failures
from utils.slider_captcha import verify_slider_captcha, SliderCaptchaError
from utils.response_codes import ResponseCode, create_standardized_error_response


class MyProtectedView(APIView):
    permission_classes = [AllowAny]

    def _get_identifier(self, request):
        """获取客户端标识(用于失败计数)"""
        x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
        if x_forwarded_for:
            return x_forwarded_for.split(',')[0].strip()
        return request.META.get('REMOTE_ADDR')

    def post(self, request):
        operation = 'my_operation'
        identifier = self._get_identifier(request)

        # 步骤1: 检查是否需要滑块验证
        if check_captcha_required(operation, identifier):
            captcha_key = request.data.get('slider_captcha_key')
            captcha_x = request.data.get('slider_captcha_x')

            if not captcha_key or captcha_x is None:
                return create_standardized_error_response(
                    code=ResponseCode.CAPTCHA_REQUIRED,
                    status_code=status.HTTP_400_BAD_REQUEST
                )

            # 步骤2: 验证滑块
            try:
                verified = verify_slider_captcha(captcha_key, int(captcha_x))
                if not verified:
                    record_failure(operation, identifier)
                    return create_standardized_error_response(
                        code=ResponseCode.CAPTCHA_ERROR,
                        status_code=status.HTTP_400_BAD_REQUEST
                    )
            except SliderCaptchaError:
                return create_standardized_error_response(
                    code=ResponseCode.CAPTCHA_EXPIRED,
                    status_code=status.HTTP_400_BAD_REQUEST
                )

        # 步骤3: 执行业务逻辑
        result = do_something(request.data)

        if result.success:
            # 成功:重置失败计数
            reset_failures(operation, identifier)
            return create_standardized_response(
                data=result.data,
                code=ResponseCode.SUCCESS,
                status_code=status.HTTP_200_OK
            )
        else:
            # 失败:记录失败次数(下次可能需要验证码)
            record_failure(operation, identifier)
            return create_standardized_error_response(
                code=ResponseCode.ERROR,
                message=result.error_message,
                status_code=status.HTTP_400_BAD_REQUEST
            )

2. 验证码触发阈值配置

在 utils/captcha.py 中定义了默认阈值:

CAPTCHA_THRESHOLD = 3  # 失败3次后开始需要验证码
CAPTCHA_TRIGGER_TTL = 600  # 触发状态持续10分钟

安全特性总结

特性 说明
一次性使用 每个 captcha_key 验证后立即失效,防止重放攻击
过期机制 验证码有效期为5分钟,过期后需重新生成
位置容差 允许±5像素的误差,提升用户体验的同时保持安全性
轨迹分析 分析拖拽速度、停顿等特征,识别自动化脚本
失败计数 连续失败达到阈值后强制要求验证码,防止暴力破解
Redis存储 使用缓存存储验证码状态,高性能且易于扩展

测试指南

运行滑块验证码相关测试

cd chunyu_project

# 运行所有滑块验证码相关测试
python manage.py test user.tests.test_slider_captcha --keepdb -v 2
python manage.py test user.tests.test_slider_captcha_api --keepdb -v 2
python manage.py test user.tests.test_login_with_slider --keepdb -v 2

# 同时运行所有测试
python manage.py test user.tests.test_slider_captcha user.tests.test_slider_captcha_api user.tests.test_login_with_slider --keepdb -v 2

测试覆盖范围

测试类 测试数 覆盖内容
TestGenerateSliderCaptcha 4 验证码生成、字段完整性、缓存存储
TestVerifySliderCaptcha 4 正确位置通过、错误位置拒绝、过期处理、容差验证
TestVerifySliderTrajectory 2 正常轨迹通过、可疑轨迹拒绝
TestSliderCaptchaAPIView 6 API端点测试、请求格式验证、响应格式验证
TestLoginWithSliderCaptcha 7 登录集成测试、验证触发、成功失败流程
总计 24

在测试中Mock滑块验证

当编写使用滑块验证码的功能测试时,可以使用 unittest.mock 来隔离验证逻辑:

from django.test import TestCase
from unittest.mock import patch
from rest_framework.test import APIClient


class MyViewTest(TestCase):
    def setUp(self):
        self.client = APIClient()

    @patch('utils.captcha.check_captcha_required')
    @patch('utils.slider_captcha.verify_slider_captcha')
    def test_with_valid_captcha(self, mock_verify, mock_check):
        # 模拟需要验证码
        mock_check.return_value = True

        # 模拟验证通过
        mock_verify.return_value = True

        response = self.client.post('/user/my-endpoint/', {
            'slider_captcha_key': 'test-key',
            'slider_captcha_x': 150,
            # 其他参数...
        }, format='json')

        # 断言verify_slider_captcha被正确调用
        mock_verify.assert_called_once_with('test-key', 150)

故障排查

常见问题

问题1: 验证码验证总是返回false

  • 检查 x_position 是否为整数类型(而非字符串)
  • 确认 captcha_key 与生成时的值一致
  • 检查滑块图片的实际位置与背景凹槽位置是否对应

问题2: 验证时返回"验证码已过期"

  • 检查是否在5分钟内完成验证
  • 检查是否已使用过该 captcha_key(一次性使用)
  • 检查Redis缓存是否正常运行

问题3: 测试中的patch不生效

  • 确保patch路径是 utils.slider_captcha.verify_slider_captcha 而非 user.views.user.verify_slider_captcha
  • 使用 format='json' 参数发送POST请求

文件位置索引

模块 路径
核心工具 utils/slider_captcha.py
API视图 user/views/slider_captcha.py
登录集成 user/views/user.py
URL配置 user/urls.py
前端JS static/js/slider-captcha.js
前端CSS static/css/slider-captcha.css
核心测试 user/tests/test_slider_captcha.py
API测试 user/tests/test_slider_captcha_api.py
登录集成测试 user/tests/test_login_with_slider.py