sync from local backup

This commit is contained in:
chunyu
2026-08-05 23:59:15 +08:00
commit d48ec1747c
358 changed files with 34384 additions and 0 deletions
+429
View File
@@ -0,0 +1,429 @@
# 滑块验证码系统使用指南
## 概述
滑块验证码是一种基于用户交互行为的验证机制,通过让用户拖动滑块到指定位置来判断操作是否为真人发起。本系统包含三个核心部分:
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
<link rel="stylesheet" href="{% static 'css/slider-captcha.css' %}">
<script src="{% static 'js/slider-captcha.js' %}"></script>
```
### 2. 添加HTML容器
```html
<div id="slider-captcha-container"></div>
```
### 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) |