# 滑块验证码系统使用指南 ## 概述 滑块验证码是一种基于用户交互行为的验证机制,通过让用户拖动滑块到指定位置来判断操作是否为真人发起。本系统包含三个核心部分: 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)** ```json { "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)** ```json { "code": 0, "message": "success", "data": { "verified": true } } ``` **错误响应 (400 Bad Request)** ```json { "code": 20022, "message": "缺少必要参数", "data": null } ``` 验证码过期错误响应: ```json { "code": 20021, "message": "验证码已过期,请重新获取", "data": null } ``` --- ## 后端验证逻辑详解 ### 1. 位置验证 ```python 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. 轨迹分析(防机器人) ```python 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. 引入静态资源 在需要滑块验证码的页面中引入: ```html ``` ### 2. 添加HTML容器 ```html
``` ### 3. 初始化组件 ```javascript 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. 在表单提交中使用 ```javascript 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](file:///c:/Users/12914/Desktop/vscode/chunyu_project/user/views/user.py) 中的 `LoginView` 实现: ```python 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` 中定义了默认阈值: ```python CAPTCHA_THRESHOLD = 3 # 失败3次后开始需要验证码 CAPTCHA_TRIGGER_TTL = 600 # 触发状态持续10分钟 ``` --- ## 安全特性总结 | 特性 | 说明 | |-----|-----| | **一次性使用** | 每个 `captcha_key` 验证后立即失效,防止重放攻击 | | **过期机制** | 验证码有效期为5分钟,过期后需重新生成 | | **位置容差** | 允许±5像素的误差,提升用户体验的同时保持安全性 | | **轨迹分析** | 分析拖拽速度、停顿等特征,识别自动化脚本 | | **失败计数** | 连续失败达到阈值后强制要求验证码,防止暴力破解 | | **Redis存储** | 使用缓存存储验证码状态,高性能且易于扩展 | --- ## 测试指南 ### 运行滑块验证码相关测试 ```bash 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` 来隔离验证逻辑: ```python 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](file:///c:/Users/12914/Desktop/vscode/chunyu_project/utils/slider_captcha.py) | | API视图 | [user/views/slider_captcha.py](file:///c:/Users/12914/Desktop/vscode/chunyu_project/user/views/slider_captcha.py) | | 登录集成 | [user/views/user.py](file:///c:/Users/12914/Desktop/vscode/chunyu_project/user/views/user.py) | | URL配置 | [user/urls.py](file:///c:/Users/12914/Desktop/vscode/chunyu_project/user/urls.py) | | 前端JS | [static/js/slider-captcha.js](file:///c:/Users/12914/Desktop/vscode/chunyu_project/static/js/slider-captcha.js) | | 前端CSS | [static/css/slider-captcha.css](file:///c:/Users/12914/Desktop/vscode/chunyu_project/static/css/slider-captcha.css) | | 核心测试 | [user/tests/test_slider_captcha.py](file:///c:/Users/12914/Desktop/vscode/chunyu_project/user/tests/test_slider_captcha.py) | | API测试 | [user/tests/test_slider_captcha_api.py](file:///c:/Users/12914/Desktop/vscode/chunyu_project/user/tests/test_slider_captcha_api.py) | | 登录集成测试 | [user/tests/test_login_with_slider.py](file:///c:/Users/12914/Desktop/vscode/chunyu_project/user/tests/test_login_with_slider.py) |