sync from local backup
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,514 @@
|
||||
# 滑块验证码后端验证实现计划
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 为现有的纯前端滑块验证码组件添加后端数据验证,提高安全性,防止前端绕过攻击。
|
||||
|
||||
**Architecture:** 采用前后端分离架构。后端负责生成验证码数据(包括加密的缺口位置)和验证用户提交的滑动位置。前端负责UI交互和与后端API通信。使用token机制确保验证码的一次性使用。
|
||||
|
||||
**Tech Stack:**
|
||||
- 前端:React, TypeScript, Ant Design, Axios
|
||||
- 后端:Django REST Framework (假设), Python, 加密库(如hashlib, hmac)
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
### 前端文件
|
||||
- Modify: `src/components/SliderCaptcha/SliderCaptcha.tsx` - 修改为从后端获取验证码数据并发送验证请求
|
||||
- Modify: `src/components/SliderCaptcha/captchaUtils.ts` - 移除本地生成逻辑,保留工具函数
|
||||
- Modify: `src/utils/request.ts` - 添加验证码API请求方法
|
||||
- Modify: `src/pages/SliderCaptchaDemo/SliderCaptchaDemo.tsx` - 更新演示页面以使用新的后端验证
|
||||
|
||||
### 后端文件(设计指导)
|
||||
- Create: `captcha/views.py` - 验证码API视图
|
||||
- Create: `captcha/serializers.py` - 数据序列化器
|
||||
- Create: `captcha/urls.py` - URL路由
|
||||
- Create: `captcha/utils.py` - 加密和验证工具函数
|
||||
|
||||
---
|
||||
|
||||
## Task 1: 后端API设计与实现
|
||||
|
||||
**Files:**
|
||||
- Create: `captcha/views.py`
|
||||
- Create: `captcha/serializers.py`
|
||||
- Create: `captcha/urls.py`
|
||||
- Create: `captcha/utils.py`
|
||||
|
||||
- [ ] **Step 1: 设计验证码数据生成算法**
|
||||
|
||||
后端需要生成验证码数据,包括:
|
||||
1. 随机背景图片URL(或使用预设图片库)
|
||||
2. 缺口位置(x, y坐标)
|
||||
3. 加密的token(包含缺口位置信息,带签名防篡改)
|
||||
4. 滑块图片(可由前端根据缺口位置生成,或后端提供)
|
||||
|
||||
加密方案:
|
||||
- 使用HMAC-SHA256对缺口位置进行签名
|
||||
- token格式:`base64(x,y):signature`
|
||||
- 密钥使用Django的SECRET_KEY
|
||||
|
||||
```python
|
||||
# captcha/utils.py
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import json
|
||||
from django.conf import settings
|
||||
|
||||
def generate_captcha_token(x: int, y: int) -> str:
|
||||
"""生成验证码token,包含加密的缺口位置"""
|
||||
data = json.dumps({'x': x, 'y': y}).encode('utf-8')
|
||||
signature = hmac.new(
|
||||
settings.SECRET_KEY.encode('utf-8'),
|
||||
data,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
token_data = base64.urlsafe_b64encode(data).decode('utf-8')
|
||||
return f"{token_data}:{signature}"
|
||||
|
||||
def verify_captcha_token(token: str, x: int, y: int, tolerance: int = 5) -> bool:
|
||||
"""验证token和用户提交的坐标"""
|
||||
try:
|
||||
token_data, signature = token.split(':')
|
||||
data = base64.urlsafe_b64decode(token_data)
|
||||
# 验证签名
|
||||
expected_signature = hmac.new(
|
||||
settings.SECRET_KEY.encode('utf-8'),
|
||||
data,
|
||||
hashlib.sha256
|
||||
).hexdigest()
|
||||
if not hmac.compare_digest(signature, expected_signature):
|
||||
return False
|
||||
# 解析原始坐标
|
||||
original = json.loads(data)
|
||||
# 验证用户坐标是否在容差范围内
|
||||
return (abs(original['x'] - x) <= tolerance and
|
||||
abs(original['y'] - y) <= tolerance)
|
||||
except Exception:
|
||||
return False
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 创建验证码API视图**
|
||||
|
||||
```python
|
||||
# captcha/views.py
|
||||
from rest_framework.views import APIView
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from django.core.cache import cache
|
||||
import random
|
||||
from .utils import generate_captcha_token, verify_captcha_token
|
||||
|
||||
class GenerateCaptchaView(APIView):
|
||||
"""生成验证码数据"""
|
||||
def get(self, request):
|
||||
# 生成随机缺口位置(实际应用中应根据图片尺寸调整)
|
||||
x = random.randint(50, 250)
|
||||
y = random.randint(20, 120)
|
||||
|
||||
# 生成token
|
||||
token = generate_captcha_token(x, y)
|
||||
|
||||
# 缓存token,有效期5分钟
|
||||
cache_key = f"captcha:{token}"
|
||||
cache.set(cache_key, {'x': x, 'y': y}, 300)
|
||||
|
||||
# 返回数据(实际应用中应返回真实图片URL)
|
||||
return Response({
|
||||
'token': token,
|
||||
'bg_image': '/static/captcha/bg1.jpg', # 示例背景图
|
||||
'gap_x': x,
|
||||
'gap_y': y,
|
||||
'width': 320,
|
||||
'height': 160,
|
||||
'gap_size': 40
|
||||
})
|
||||
|
||||
class VerifyCaptchaView(APIView):
|
||||
"""验证用户提交的滑块位置"""
|
||||
def post(self, request):
|
||||
token = request.data.get('token')
|
||||
user_x = request.data.get('x')
|
||||
user_y = request.data.get('y')
|
||||
|
||||
if not all([token, user_x is not None, user_y is not None]):
|
||||
return Response(
|
||||
{'error': '缺少必要参数'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 从缓存获取原始数据(可选,用于额外验证)
|
||||
cache_key = f"captcha:{token}"
|
||||
cached_data = cache.get(cache_key)
|
||||
|
||||
if not cached_data:
|
||||
return Response(
|
||||
{'error': '验证码已过期或无效'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 验证token和坐标
|
||||
if verify_captcha_token(token, int(user_x), int(user_y)):
|
||||
# 验证成功,删除缓存(一次性使用)
|
||||
cache.delete(cache_key)
|
||||
return Response({'success': True, 'message': '验证成功'})
|
||||
else:
|
||||
return Response(
|
||||
{'success': False, 'message': '验证失败'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 创建URL路由**
|
||||
|
||||
```python
|
||||
# captcha/urls.py
|
||||
from django.urls import path
|
||||
from .views import GenerateCaptchaView, VerifyCaptchaView
|
||||
|
||||
urlpatterns = [
|
||||
path('generate/', GenerateCaptchaView.as_view(), name='generate-captcha'),
|
||||
path('verify/', VerifyCaptchaView.as_view(), name='verify-captcha'),
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 在Django项目中注册路由**
|
||||
|
||||
在项目的`urls.py`中添加:
|
||||
```python
|
||||
from django.urls import path, include
|
||||
|
||||
urlpatterns = [
|
||||
# ... 其他路由
|
||||
path('api/captcha/', include('captcha.urls')),
|
||||
]
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 创建序列化器(可选,用于数据验证)**
|
||||
|
||||
```python
|
||||
# captcha/serializers.py
|
||||
from rest_framework import serializers
|
||||
|
||||
class VerifyCaptchaSerializer(serializers.Serializer):
|
||||
token = serializers.CharField(required=True)
|
||||
x = serializers.IntegerField(required=True)
|
||||
y = serializers.IntegerField(required=True)
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 提供示例背景图片**
|
||||
|
||||
在`static/captcha/`目录下放置几张背景图片,用于验证码显示。
|
||||
|
||||
---
|
||||
|
||||
## Task 2: 前端API集成
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/utils/request.ts`
|
||||
- Modify: `src/components/SliderCaptcha/SliderCaptcha.tsx`
|
||||
- Modify: `src/components/SliderCaptcha/captchaUtils.ts`
|
||||
|
||||
- [ ] **Step 1: 在request.ts中添加验证码API方法**
|
||||
|
||||
在`api_request`对象中添加captcha模块:
|
||||
|
||||
```typescript
|
||||
// 在request.ts的api_request对象中添加
|
||||
const captcha = {
|
||||
generate: () =>
|
||||
request.public.get("/api/captcha/generate/"),
|
||||
|
||||
verify: (data: { token: string; x: number; y: number }) =>
|
||||
request.public.post("/api/captcha/verify/", data),
|
||||
};
|
||||
|
||||
export const api_request = {
|
||||
// ... 其他模块
|
||||
captcha, // 添加这一行
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 修改SliderCaptcha组件,使用后端API**
|
||||
|
||||
修改`SliderCaptcha.tsx`,主要变更:
|
||||
1. `loadCaptcha`函数改为从后端获取数据
|
||||
2. `doVerify`函数改为发送验证请求到后端
|
||||
3. 添加token状态管理
|
||||
4. 处理API错误
|
||||
|
||||
```typescript
|
||||
// SliderCaptcha.tsx 关键修改部分
|
||||
const SliderCaptcha: React.FC<SliderCaptchaProps> = ({
|
||||
width = 320,
|
||||
height = 160,
|
||||
gapSize = 40,
|
||||
tolerance = 5,
|
||||
onSuccess,
|
||||
onFail,
|
||||
onRefresh,
|
||||
className = "",
|
||||
style = {},
|
||||
}) => {
|
||||
const [status, setStatus] = useState<CaptchaStatus>("loading");
|
||||
const [captchaData, setCaptchaData] = useState<CaptchaResult | null>(null);
|
||||
const [sliderX, setSliderX] = useState(0);
|
||||
const [message, setMessage] = useState("向右滑动完成验证");
|
||||
const [token, setToken] = useState<string>(""); // 新增token状态
|
||||
|
||||
// ... 其他状态和ref
|
||||
|
||||
const loadCaptcha = useCallback(async () => {
|
||||
setStatus("loading");
|
||||
setSliderX(0);
|
||||
setMessage("向右滑动完成验证");
|
||||
try {
|
||||
// 从后端获取验证码数据
|
||||
const response = await api_request.captcha.generate();
|
||||
const data = response.data;
|
||||
|
||||
// 使用后端返回的数据生成前端验证码
|
||||
const captchaResult = await generateCaptcha(
|
||||
data.bg_image,
|
||||
data.width,
|
||||
data.height,
|
||||
data.gap_size
|
||||
);
|
||||
|
||||
setCaptchaData(captchaResult);
|
||||
setToken(data.token); // 保存token
|
||||
setStatus("ready");
|
||||
} catch (error) {
|
||||
console.error("获取验证码失败:", error);
|
||||
setMessage("验证码加载失败,请刷新重试");
|
||||
setStatus("fail");
|
||||
}
|
||||
}, []);
|
||||
|
||||
const doVerify = useCallback(async () => {
|
||||
if (!isDraggingRef.current || !captchaData || !token) return;
|
||||
isDraggingRef.current = false;
|
||||
setStatus("verifying");
|
||||
|
||||
try {
|
||||
// 发送到后端验证
|
||||
const response = await api_request.captcha.verify({
|
||||
token,
|
||||
x: sliderX,
|
||||
y: captchaData.gapY // 如果后端需要Y坐标
|
||||
});
|
||||
|
||||
if (response.data.success) {
|
||||
setStatus("success");
|
||||
setMessage("验证成功");
|
||||
onSuccess?.();
|
||||
} else {
|
||||
setStatus("fail");
|
||||
setMessage("验证失败,请重试");
|
||||
setSliderX(0);
|
||||
onFail?.();
|
||||
setTimeout(() => {
|
||||
handleRefresh();
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("验证请求失败:", error);
|
||||
setStatus("fail");
|
||||
setMessage("验证失败,请重试");
|
||||
setSliderX(0);
|
||||
onFail?.();
|
||||
setTimeout(() => {
|
||||
handleRefresh();
|
||||
}, 1000);
|
||||
}
|
||||
}, [captchaData, sliderX, token, onSuccess, onFail]);
|
||||
|
||||
// ... 其他代码保持不变
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 更新captchaUtils.ts**
|
||||
|
||||
移除本地生成逻辑,但保留`generateCaptcha`函数用于前端渲染。实际上,我们可以保留现有函数,因为它只是用于生成Canvas图像,不涉及安全逻辑。
|
||||
|
||||
但需要修改`getRandomImage`函数,因为图片现在由后端提供:
|
||||
|
||||
```typescript
|
||||
// captchaUtils.ts 修改
|
||||
/**
|
||||
* 获取验证码图片(现在由后端提供,此函数可保留用于兼容)
|
||||
*/
|
||||
export const getRandomImage = (width: number, height: number): string => {
|
||||
// 实际应用中,这个URL应该来自后端API
|
||||
// 这里保留作为fallback
|
||||
const seed = randomInt(1, 1000);
|
||||
return `https://picsum.photos/seed/${seed}/${width}/${height}`;
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 更新演示页面**
|
||||
|
||||
修改`SliderCaptchaDemo.tsx`,展示后端验证的使用方式,并更新说明文档。
|
||||
|
||||
---
|
||||
|
||||
## Task 3: 安全增强与测试
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/components/SliderCaptcha/SliderCaptcha.tsx`
|
||||
- Create: 测试用例
|
||||
|
||||
- [ ] **Step 1: 添加请求频率限制**
|
||||
|
||||
在前端添加请求间隔限制,防止暴力破解:
|
||||
|
||||
```typescript
|
||||
// SliderCaptcha.tsx 中添加
|
||||
const lastVerifyTime = useRef(0);
|
||||
const VERIFY_COOLDOWN = 1000; // 1秒冷却时间
|
||||
|
||||
const doVerify = useCallback(async () => {
|
||||
const now = Date.now();
|
||||
if (now - lastVerifyTime.current < VERIFY_COOLDOWN) {
|
||||
setMessage("操作过于频繁");
|
||||
return;
|
||||
}
|
||||
lastVerifyTime.current = now;
|
||||
|
||||
// ... 验证逻辑
|
||||
}, []);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 添加错误处理和重试机制**
|
||||
|
||||
```typescript
|
||||
// SliderCaptcha.tsx 中添加重试逻辑
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
const doVerify = useCallback(async () => {
|
||||
// ... 验证逻辑
|
||||
|
||||
if (!response.data.success) {
|
||||
if (retryCount < MAX_RETRIES) {
|
||||
setRetryCount(prev => prev + 1);
|
||||
setMessage(`验证失败,还有${MAX_RETRIES - retryCount}次机会`);
|
||||
} else {
|
||||
setMessage("验证次数已用完,请刷新页面");
|
||||
setStatus("fail");
|
||||
}
|
||||
}
|
||||
}, [retryCount]);
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 后端安全增强**
|
||||
|
||||
1. 添加请求频率限制(Django Ratelimit)
|
||||
2. 记录验证尝试次数
|
||||
3. 添加IP黑名单机制
|
||||
4. 使用HTTPS传输
|
||||
|
||||
```python
|
||||
# 后端安全增强示例
|
||||
from django.core.cache import cache
|
||||
from django_ratelimit.decorators import ratelimit
|
||||
|
||||
class VerifyCaptchaView(APIView):
|
||||
@ratelimit(key='ip', rate='5/m', method='POST')
|
||||
def post(self, request):
|
||||
# ... 验证逻辑
|
||||
pass
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 编写单元测试**
|
||||
|
||||
为后端API编写测试用例,确保:
|
||||
1. token生成和验证的正确性
|
||||
2. 过期token的处理
|
||||
3. 错误坐标的验证
|
||||
4. 频繁请求的限制
|
||||
|
||||
---
|
||||
|
||||
## Task 4: 部署与监控
|
||||
|
||||
**Files:**
|
||||
- Modify: Django设置文件
|
||||
- Create: 监控脚本
|
||||
|
||||
- [ ] **Step 1: 配置缓存后端**
|
||||
|
||||
确保Django使用Redis或Memcached作为缓存后端,用于存储验证码token:
|
||||
|
||||
```python
|
||||
# settings.py
|
||||
CACHES = {
|
||||
'default': {
|
||||
'BACKEND': 'django_redis.cache.RedisCache',
|
||||
'LOCATION': 'redis://127.0.0.1:6379/1',
|
||||
'OPTIONS': {
|
||||
'CLIENT_CLASS': 'django_redis.client.DefaultClient',
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 添加日志记录**
|
||||
|
||||
记录验证码生成和验证事件,用于监控和调试:
|
||||
|
||||
```python
|
||||
# views.py 中添加日志
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class GenerateCaptchaView(APIView):
|
||||
def get(self, request):
|
||||
logger.info(f"验证码生成: IP={request.META.get('REMOTE_ADDR')}")
|
||||
# ... 生成逻辑
|
||||
|
||||
class VerifyCaptchaView(APIView):
|
||||
def post(self, request):
|
||||
logger.info(f"验证码验证: IP={request.META.get('REMOTE_ADDR')}, token={token[:10]}...")
|
||||
# ... 验证逻辑
|
||||
```
|
||||
|
||||
- [ ] **Step 3: 监控和告警**
|
||||
|
||||
设置监控指标:
|
||||
1. 验证码生成频率
|
||||
2. 验证成功率
|
||||
3. 失败尝试次数
|
||||
4. 异常请求模式
|
||||
|
||||
---
|
||||
|
||||
## 执行选项
|
||||
|
||||
**计划已保存到 `docs/plans/2026-06-13-slider-captcha-backend-verification.md`。两种执行方式:**
|
||||
|
||||
**1. Subagent-Driven(推荐)** - 我为每个Task分配一个子代理,任务间进行审查,快速迭代
|
||||
|
||||
**2. Inline Execution** - 在当前会话中执行任务,使用executing-plans,批量执行并设置检查点
|
||||
|
||||
**请选择执行方式?**
|
||||
|
||||
**如果选择Subagent-Driven:**
|
||||
- **必需子技能:** 使用superpowers:subagent-driven-development
|
||||
- 每个Task一个子代理 + 两阶段审查
|
||||
|
||||
**如果选择Inline Execution:**
|
||||
- **必需子技能:** 使用superpowers:executing-plans
|
||||
- 批量执行并设置检查点供审查
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **后端实现需要Django项目配合**:本计划假设后端使用Django REST Framework,需要后端开发人员配合实现。
|
||||
2. **图片资源**:需要准备验证码背景图片,或使用图片生成服务。
|
||||
3. **安全性考虑**:务必使用HTTPS,保护SECRET_KEY,定期轮换密钥。
|
||||
4. **性能优化**:考虑使用CDN加速图片加载,缓存验证码数据。
|
||||
5. **用户体验**:添加加载动画、错误提示、重试机制。
|
||||
Reference in New Issue
Block a user