Files
chunyu_prject_react/docs/plans/2026-06-13-slider-captcha-backend-verification.md
2026-08-05 23:59:22 +08:00

15 KiB
Raw Permalink Blame History

滑块验证码后端验证实现计划

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
# 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视图
# 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路由
# 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中添加:

from django.urls import path, include

urlpatterns = [
    # ... 其他路由
    path('api/captcha/', include('captcha.urls')),
]
  • Step 5: 创建序列化器(可选,用于数据验证)
# 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模块:

// 在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错误
// 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函数,因为图片现在由后端提供:

// 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: 添加请求频率限制

在前端添加请求间隔限制,防止暴力破解:

// 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: 添加错误处理和重试机制
// 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传输
# 后端安全增强示例
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:

# 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: 添加日志记录

记录验证码生成和验证事件,用于监控和调试:

# 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. 用户体验:添加加载动画、错误提示、重试机制。