Laravel 12 中拼图验证码的防机器人攻击实现策略

拼图验证码是一种有效的防机器人验证方式,但要真正防止自动化攻击,需要多层次的防护措施。以下是完整的实现方案:

一、基础拼图验证码实现

1. 后端生成逻辑

namespace App\Services;

class JigsawCaptcha
{
    public function generate()
    {
        // 生成随机位置 (10%-90% 范围内)
        $position = rand(10, 90);
        $token = Str::random(32);
        
        // 存储到缓存,5分钟有效期
        Cache::put('jigsaw_'.$token, $position, now()->addMinutes(5));
        
        // 生成带缺口的背景图
        $bgImage = imagecreatetruecolor(300, 150);
        $bgColor = imagecolorallocate($bgImage, 245, 245, 245);
        imagefill($bgImage, 0, 0, $bgColor);
        
        // 添加干扰元素
        $this->addNoise($bgImage);
        
        // 生成拼图块
        $piece = $this->generatePiece($position);
        
        // 合并图像
        imagecopy($bgImage, $piece, $position, 50, 0, 0, 40, 40);
        
        // 输出图像
        ob_start();
        imagepng($bgImage);
        $bgData = ob_get_clean();
        
        return [
            'token' => $token,
            'bg_image' => 'data:image/png;base64,'.base64_encode($bgData),
            'piece_image' => 'data:image/png;base64,'.base64_encode($piece),
            'piece_y' => 50 // 拼图垂直位置
        ];
    }
    
    protected function addNoise($image)
    {
        // 添加干扰线
        for ($i = 0; $i < 5; $i++) {
            $color = imagecolorallocate($image, rand(100, 200), rand(100, 200), rand(100, 200));
            imageline($image, rand(0, 300), rand(0, 150), rand(0, 300), rand(0, 150), $color);
        }
        
        // 添加干扰点
        for ($i = 0; $i < 100; $i++) {
            $color = imagecolorallocate($image, rand(100, 200), rand(100, 200), rand(100, 200));
            imagesetpixel($image, rand(0, 300), rand(0, 150), $color);
        }
    }
    
    protected function generatePiece($position)
    {
        $piece = imagecreatetruecolor(40, 40);
        $bgColor = imagecolorallocate($piece, 255, 255, 255);
        imagefill($piece, 0, 0, $bgColor);
        
        // 生成拼图形状
        $shapeColor = imagecolorallocate($piece, 70, 130, 180);
        imagefilledellipse($piece, 20, 20, 38, 38, $shapeColor);
        
        // 添加高光效果
        $highlight = imagecolorallocatealpha($piece, 255, 255, 255, 80);
        imagefilledellipse($piece, 12, 12, 20, 20, $highlight);
        
        return $piece;
    }
    
    public function validate($token, $userPosition)
    {
        $savedPosition = Cache::get('jigsaw_'.$token);
        
        if (!$savedPosition) {
            return false;
        }
        
        Cache::forget('jigsaw_'.$token);
        
        // 允许±5像素的误差
        return abs($userPosition - $savedPosition) <= 5;
    }
}

2. 控制器实现

namespace App\Http\Controllers;

use App\Services\JigsawCaptcha;
use Illuminate\Http\Request;

class CaptchaController extends Controller
{
    public function getJigsaw(JigsawCaptcha $captcha)
    {
        return response()->json($captcha->generate());
    }
    
    public function verifyJigsaw(Request $request, JigsawCaptcha $captcha)
    {
        $validated = $request->validate([
            'token' => 'required|string',
            'position' => 'required|numeric'
        ]);
        
        if ($captcha->validate($validated['token'], $validated['position'])) {
            return response()->json(['success' => true]);
        }
        
        return response()->json(['success' => false], 401);
    }
}

二、防机器人增强策略

1. 行为分析防护

// 在验证方法中添加行为分析
public function verifyJigsaw(Request $request, JigsawCaptcha $captcha)
{
    $validated = $request->validate([
        'token' => 'required|string',
        'position' => 'required|numeric',
        'behavior_data' => 'required|array' // 前端收集的行为数据
    ]);
    
    // 基础验证
    if (!$captcha->validate($validated['token'], $validated['position'])) {
        return response()->json(['success' => false], 401);
    }
    
    // 行为分析验证
    if (!$this->analyzeBehavior($validated['behavior_data'])) {
        return response()->json(['success' => false, 'reason' => 'suspicious_behavior'], 401);
    }
    
    return response()->json(['success' => true]);
}

protected function analyzeBehavior($behaviorData)
{
    // 1. 拖动时间分析 (人类通常需要0.5-3秒)
    $dragTime = $behaviorData['drag_time_ms'];
    if ($dragTime < 300 || $dragTime > 5000) {
        return false;
    }
    
    // 2. 拖动路径分析 (机器人通常是直线)
    $path = $behaviorData['path'];
    $pathComplexity = $this->calculatePathComplexity($path);
    if ($pathComplexity < 0.7) { // 路径复杂度阈值
        return false;
    }
    
    // 3. 鼠标移动分析
    $movements = $behaviorData['movements'];
    $randomness = $this->calculateMovementRandomness($movements);
    if ($randomness < 0.6) {
        return false;
    }
    
    // 4. 加速度分析
    $accelerations = $behaviorData['accelerations'];
    if (max($accelerations) > 0.3) { // 最大加速度阈值
        return false;
    }
    
    return true;
}

2. 前端行为数据收集

class BehaviorTracker {
    constructor() {
        this.startTime = 0;
        this.path = [];
        this.movements = [];
        this.lastPosition = null;
        this.lastTimestamp = 0;
    }
    
    startTracking() {
        this.startTime = Date.now();
        this.path = [];
        this.movements = [];
    }
    
    recordMove(x, y) {
        const now = Date.now();
        this.path.push({x, y, time: now});
        
        if (this.lastPosition) {
            const distance = Math.sqrt(
                Math.pow(x - this.lastPosition.x, 2) + 
                Math.pow(y - this.lastPosition.y, 2)
            );
            const timeDiff = now - this.lastTimestamp;
            this.movements.push({
                distance,
                time: timeDiff,
                speed: distance / (timeDiff || 1)
            });
        }
        
        this.lastPosition = {x, y};
        this.lastTimestamp = now;
    }
    
    getBehaviorData() {
        const dragTime = Date.now() - this.startTime;
        
        // 计算加速度
        const accelerations = [];
        for (let i = 1; i < this.movements.length; i++) {
            const accel = (this.movements[i].speed - this.movements[i-1].speed) / 
                         (this.movements[i].time || 1);
            accelerations.push(accel);
        }
        
        return {
            drag_time_ms: dragTime,
            path: this.path,
            movements: this.movements,
            accelerations: accelerations
        };
    }
}

3. 频率限制防护

// 在控制器中添加中间件
public function __construct()
{
    $this->middleware('throttle:5,1')->only(['getJigsaw', 'verifyJigsaw']);
}

// 或者在路由中定义
Route::middleware('throttle:5,1')->group(function() {
    Route::get('/jigsaw', [CaptchaController::class, 'getJigsaw']);
    Route::post('/verify-jigsaw', [CaptchaController::class, 'verifyJigsaw']);
});

4. IP信誉系统

// 数据库迁移
Schema::create('ip_reputation', function (Blueprint $table) {
    $table->string('ip', 45)->primary();
    $table->integer('failed_attempts')->default(0);
    $table->timestamp('last_attempt')->nullable();
    $table->timestamp('blocked_until')->nullable();
});

// 在验证前检查IP信誉
public function verifyJigsaw(Request $request, JigsawCaptcha $captcha)
{
    $ip = $request->ip();
    $reputation = DB::table('ip_reputation')->where('ip', $ip)->first();
    
    // 检查是否被临时封禁
    if ($reputation && $reputation->blocked_until && now()->lt($reputation->blocked_until)) {
        return response()->json([
            'success' => false,
            'reason' => 'ip_blocked',
            'retry_after' => $reputation->blocked_until->diffForHumans()
        ], 403);
    }
    
    // ...原有验证逻辑...
    
    // 验证失败时更新IP信誉
    if (!/* 验证成功条件 */) {
        $this->updateIpReputation($ip, false);
        return response()->json(['success' => false], 401);
    }
    
    $this->updateIpReputation($ip, true);
    return response()->json(['success' => true]);
}

protected function updateIpReputation($ip, $success)
{
    $reputation = DB::table('ip_reputation')->where('ip', $ip)->first();
    
    if ($success) {
        // 成功验证时重置计数
        if ($reputation) {
            DB::table('ip_reputation')
                ->where('ip', $ip)
                ->update([
                    'failed_attempts' => 0,
                    'last_attempt' => now()
                ]);
        }
    } else {
        // 失败时增加计数
        $failedAttempts = $reputation ? $reputation->failed_attempts + 1 : 1;
        
        $blockedUntil = null;
        if ($failedAttempts >= 5) {
            $blockMinutes = min(60 * 24, pow(2, $failedAttempts - 5) * 5);
            $blockedUntil = now()->addMinutes($blockMinutes);
        }
        
        DB::table('ip_reputation')->updateOrInsert(
            ['ip' => $ip],
            [
                'failed_attempts' => $failedAttempts,
                'last_attempt' => now(),
                'blocked_until' => $blockedUntil
            ]
        );
    }
}

三、高级防护措施

1. 动态拼图难度调整

// 根据IP信誉调整难度
public function generate(Request $request)
{
    $ip = $request->ip();
    $reputation = DB::table('ip_reputation')->where('ip', $ip)->first();
    
    $baseDifficulty = 1; // 1-10级别
    
    if ($reputation) {
        $baseDifficulty += min(5, floor($reputation->failed_attempts / 2));
    }
    
    // 根据难度调整参数
    $params = [
        'noise_level' => $baseDifficulty * 2,
        'piece_complexity' => min(5, ceil($baseDifficulty / 2)),
        'tolerance' => max(2, 10 - $baseDifficulty) // 允许的误差范围
    ];
    
    // ...生成拼图...
}

2. Canvas指纹验证

// 前端收集Canvas指纹
function getCanvasFingerprint() {
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    
    canvas.width = 200;
    canvas.height = 50;
    
    ctx.textBaseline = 'top';
    ctx.font = '14px Arial';
    ctx.fillStyle = '#f60';
    ctx.fillRect(0, 0, 200, 50);
    ctx.fillStyle = '#069';
    ctx.fillText('Canvas Fingerprint', 2, 15);
    
    return canvas.toDataURL();
}

// 发送验证请求时包含指纹
fetch('/verify-jigsaw', {
    method: 'POST',
    body: JSON.stringify({
        // ...其他数据...
        canvas_fingerprint: getCanvasFingerprint()
    })
});

3. WebGL指纹验证

function getWebGLFingerprint() {
    const gl = document.createElement('canvas').getContext('webgl');
    if (!gl) return null;
    
    const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
    return {
        vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
        renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL),
        // 其他WebGL参数...
    };
}

四、部署建议

  1. 多因素验证:对高风险操作结合短信/邮件验证码
  2. 定期更新算法:每月更新拼图生成算法防止被破解
  3. 监控系统:记录验证失败模式,检测自动化攻击
  4. 分布式防御:使用Cloudflare等服务的机器人防护
  5. 机器学习模型:收集正常用户行为数据训练识别模型

五、前端完整实现示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>拼图验证码</title>
    <style>
        #captcha-container {
            width: 350px;
            margin: 50px auto;
            font-family: Arial, sans-serif;
            position: relative;
        }
        
        .captcha-bg {
            width: 350px;
            height: 180px;
            border: 1px solid #ddd;
            position: relative;
            overflow: hidden;
        }
        
        #captcha-bg {
            width: 100%;
            height: 100%;
            object-fit: cover;
        }
        
        .captcha-piece {
            position: absolute;
            width: 50px;
            height: 50px;
            cursor: move;
            z-index: 10;
            transition: left 0.1s ease-out;
        }
        
        .captcha-piece img {
            width: 100%;
            height: 100%;
            pointer-events: none;
        }
        
        .captcha-slider {
            margin-top: 20px;
            position: relative;
            height: 40px;
        }
        
        .slider-track {
            width: 100%;
            height: 8px;
            background: #e0e0e0;
            border-radius: 4px;
            position: absolute;
            top: 16px;
        }
        
        .slider-btn {
            width: 40px;
            height: 40px;
            background: #4CAF50;
            border-radius: 50%;
            position: absolute;
            left: 0;
            top: 0;
            cursor: grab;
            box-shadow: 0 2px 5px rgba(0,0,0,0.2);
            z-index: 20;
            display: flex;
            align-items: center;
            justify-content: center;
            color: white;
            font-size: 20px;
            user-select: none;
        }
        
        .slider-btn:active {
            cursor: grabbing;
            background: #388E3C;
        }
        
        .captcha-tips {
            margin-top: 10px;
            text-align: center;
            color: #666;
            font-size: 14px;
        }
        
        .captcha-success {
            color: #4CAF50;
            font-weight: bold;
        }
        
        .captcha-error {
            color: #F44336;
        }
    </style>
</head>
<body>
    <div id="captcha-container">
        <div class="captcha-bg">
            <img id="captcha-bg" src="" alt="验证背景">
        </div>
        <div class="captcha-piece" id="captcha-piece">
            <img src="" alt="拼图块">
        </div>
        <div class="captcha-slider">
            <div class="slider-track"></div>
            <div class="slider-btn" id="slider-btn"></div>
        </div>
        <div class="captcha-tips" id="captcha-tips">正在加载验证码...</div>
    </div>

    <script>
        class BehaviorTracker {
            constructor() {
                this.startTime = 0;
                this.events = [];
                this.deviceInfo = {};
            }
            
            startTracking() {
                this.startTime = Date.now();
                this.events = [];
                this.collectDeviceInfo();
            }
            
            collectDeviceInfo() {
                this.deviceInfo = {
                    screenWidth: window.screen.width,
                    screenHeight: window.screen.height,
                    colorDepth: window.screen.colorDepth,
                    timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
                    touchSupport: 'ontouchstart' in window,
                    hardwareConcurrency: navigator.hardwareConcurrency || 0,
                    deviceMemory: navigator.deviceMemory || 0,
                    userAgent: navigator.userAgent,
                    language: navigator.language,
                    languages: navigator.languages,
                    doNotTrack: navigator.doNotTrack,
                    plugins: Array.from(navigator.plugins).map(p => p.name).join(','),
                    webglVendor: this.getWebGLInfo()
                };
            }
            
            getWebGLInfo() {
                try {
                    const canvas = document.createElement('canvas');
                    const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
                    if (!gl) return null;
                    
                    const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
                    return debugInfo ? {
                        vendor: gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL),
                        renderer: gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
                    } : null;
                } catch (e) {
                    return null;
                }
            }
            
            recordMove(x, y) {
                this.events.push({
                    type: 'move',
                    x,
                    y,
                    timestamp: Date.now() - this.startTime
                });
            }
            
            getBehaviorData() {
                return {
                    events: this.events,
                    deviceInfo: this.deviceInfo,
                    duration: Date.now() - this.startTime
                };
            }
        }

        class JigsawCaptcha {
            constructor() {
                this.tracker = new BehaviorTracker();
                this.captchaData = null;
                this.isDragging = false;
                this.startX = 0;
                this.currentX = 0;
                
                this.init();
            }
            
            async init() {
                await this.loadCaptcha();
                this.setupEventListeners();
            }
            
            async loadCaptcha() {
                try {
                    const tips = document.getElementById('captcha-tips');
                    tips.textContent = '正在加载验证码...';
                    
                    // 添加随机参数防止缓存
                    const response = await fetch('/api/captcha/jigsaw?_=' + Date.now());
                    if (!response.ok) throw new Error('网络响应不正常');
                    
                    this.captchaData = await response.json();
                    
                    document.getElementById('captcha-bg').src = this.captchaData.bg_image;
                    document.getElementById('captcha-piece').querySelector('img').src = this.captchaData.piece_image;
                    document.getElementById('captcha-tips').textContent = '请拖动滑块将拼图放入正确位置';
                    
                    // 设置拼图块初始位置
                    const piece = document.getElementById('captcha-piece');
                    piece.style.top = `${this.captchaData.piece_y}px`;
                    piece.style.left = '0px';
                } catch (error) {
                    console.error('加载验证码失败:', error);
                    document.getElementById('captcha-tips').textContent = '验证码加载失败,请刷新页面重试';
                    document.getElementById('captcha-tips').className = 'captcha-tips captcha-error';
                }
            }
            
            setupEventListeners() {
                const sliderBtn = document.getElementById('slider-btn');
                
                // 鼠标事件
                sliderBtn.addEventListener('mousedown', (e) => {
                    e.preventDefault();
                    this.isDragging = true;
                    this.startX = e.clientX;
                    this.tracker.startTracking();
                    document.addEventListener('mousemove', this.onDrag.bind(this));
                    document.addEventListener('mouseup', this.onDragEnd.bind(this));
                });
                
                // 触摸屏支持
                sliderBtn.addEventListener('touchstart', (e) => {
                    e.preventDefault();
                    this.isDragging = true;
                    this.startX = e.touches[0].clientX;
                    this.tracker.startTracking();
                    document.addEventListener('touchmove', this.onTouchDrag.bind(this));
                    document.addEventListener('touchend', this.onDragEnd.bind(this));
                });
                
                // 防止拖动时选中文本
                document.addEventListener('selectstart', (e) => {
                    if (this.isDragging) e.preventDefault();
                });
            }
            
            onDrag(e) {
                if (!this.isDragging) return;
                e.preventDefault();
                
                this.currentX = e.clientX - this.startX;
                this.updatePosition();
                this.tracker.recordMove(e.clientX, e.clientY);
            }
            
            onTouchDrag(e) {
                if (!this.isDragging) return;
                e.preventDefault();
                
                this.currentX = e.touches[0].clientX - this.startX;
                this.updatePosition();
                this.tracker.recordMove(e.touches[0].clientX, e.touches[0].clientY);
            }
            
            updatePosition() {
                const sliderTrack = document.querySelector('.slider-track');
                const sliderBtn = document.getElementById('slider-btn');
                const maxX = sliderTrack.offsetWidth - sliderBtn.offsetWidth;
                
                // 限制范围
                this.currentX = Math.max(0, Math.min(this.currentX, maxX));
                
                // 更新滑块位置
                sliderBtn.style.transform = `translateX(${this.currentX}px)`;
                
                // 更新拼图位置
                const piece = document.getElementById('captcha-piece');
                piece.style.left = `${this.currentX}px`;
            }
            
            async onDragEnd() {
                if (!this.isDragging) return;
                this.isDragging = false;
                
                // 移除事件监听
                document.removeEventListener('mousemove', this.onDrag);
                document.removeEventListener('mouseup', this.onDragEnd);
                document.removeEventListener('touchmove', this.onTouchDrag);
                document.removeEventListener('touchend', this.onDragEnd);
                
                // 计算实际位置 (百分比)
                const sliderTrack = document.querySelector('.slider-track');
                const maxX = sliderTrack.offsetWidth - document.getElementById('slider-btn').offsetWidth;
                const positionPercent = (this.currentX / maxX) * 100;
                
                // 收集行为数据
                const behaviorData = this.tracker.getBehaviorData();
                
                // 显示验证中状态
                const tips = document.getElementById('captcha-tips');
                tips.textContent = '正在验证...';
                
                try {
                    // 发送验证请求
                    const response = await fetch('/api/captcha/verify', {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json',
                            'X-Requested-With': 'XMLHttpRequest'
                        },
                        body: JSON.stringify({
                            token: this.captchaData.token,
                            position: positionPercent,
                            behavior: behaviorData,
                            timestamp: Math.floor(Date.now() / 1000)
                        })
                    });
                    
                    const result = await response.json();
                    
                    if (result.success) {
                        tips.textContent = '验证成功!';
                        tips.className = 'captcha-tips captcha-success';
                        
                        // 触发自定义事件通知验证成功
                        document.dispatchEvent(new CustomEvent('captchaSuccess', {
                            detail: { authToken: result.auth_token }
                        }));
                    } else {
                        tips.textContent = result.error || '验证失败,请重试';
                        tips.className = 'captcha-tips captcha-error';
                        
                        // 重置验证码
                        setTimeout(() => {
                            this.loadCaptcha();
                            this.resetSlider();
                        }, 1000);
                    }
                } catch (error) {
                    console.error('验证请求失败:', error);
                    tips.textContent = '网络错误,请重试';
                    tips.className = 'captcha-tips captcha-error';
                }
            }
            
            resetSlider() {
                this.currentX = 0;
                document.getElementById('slider-btn').style.transform = 'translateX(0)';
                document.getElementById('captcha-piece').style.left = '0px';
            }
        }

        // 初始化验证码
        document.addEventListener('DOMContentLoaded', () => {
            new JigsawCaptcha();
            
            // 监听验证成功事件
            document.addEventListener('captchaSuccess', (e) => {
                console.log('验证成功,令牌:', e.detail.authToken);
                // 这里可以将令牌存储或发送到后端进行后续验证
            });
        });

        // 反调试保护
        function detectDevTools() {
            const threshold = 160;
            
            function check() {
                if (window.outerWidth - window.innerWidth > threshold || 
                    window.outerHeight - window.innerHeight > threshold) {
                    document.body.innerHTML = '<h1 style="text-align:center;margin-top:100px;">请关闭开发者工具后再继续操作</h1>';
                    window.location.reload();
                }
            }
            
            setInterval(check, 1000);
            window.addEventListener('resize', check);
        }
        
        // 禁用右键菜单和键盘快捷键
        document.addEventListener('contextmenu', e => e.preventDefault());
        document.addEventListener('keydown', e => {
            if (e.ctrlKey && e.shiftKey && e.key === 'I') e.preventDefault();
            if (e.ctrlKey && e.shiftKey && e.key === 'J') e.preventDefault();
            if (e.ctrlKey && e.key === 'U') e.preventDefault();
        });
        
        // 可选:启用反调试保护
        // detectDevTools();
    </script>
</body>
</html>

六、行为分析与机器学习防护

1. 行为特征收集

// 扩展行为数据收集
protected function collectBehaviorData(Request $request)
{
    $data = [
        'timestamp' => now()->toDateTimeString(),
        'ip' => $request->ip(),
        'user_agent' => $request->userAgent(),
        'accept_language' => $request->header('Accept-Language'),
        'screen_resolution' => $request->input('screen_resolution'),
        'timezone' => $request->input('timezone'),
        'plugins' => $request->input('plugins'),
        'fonts' => $request->input('fonts'),
        'canvas_hash' => $request->input('canvas_hash'),
        'webgl_hash' => $request->input('webgl_hash'),
        'drag_events' => $request->input('drag_events'),
        'mouse_movements' => $request->input('mouse_movements'),
        'timing' => [
            'load_time' => $request->input('timing.load_time'),
            'drag_duration' => $request->input('timing.drag_duration'),
            'response_time' => $request->input('timing.response_time')
        ],
        'device_info' => [
            'touch_support' => $request->input('device_info.touch_support'),
            'device_memory' => $request->input('device_info.device_memory'),
            'hardware_concurrency' => $request->input('device_info.hardware_concurrency')
        ]
    ];
    
    // 存储行为数据用于分析
    DB::table('captcha_behavior_logs')->insert([
        'data' => json_encode($data),
        'is_human' => null, // 初始为null,后续由管理员或系统标记
        'created_at' => now(),
        'updated_at' => now()
    ]);
    
    return $data;
}

2. 机器学习模型集成

// 使用预训练的模型进行实时评分
protected function evaluateWithMLModel($behaviorData)
{
    // 这里应该是调用机器学习模型的代码
    // 实际项目中可以使用TensorFlow Serving或自定义模型
    
    // 示例特征向量
    $features = [
        'drag_speed_variance' => $this->calculateSpeedVariance($behaviorData['drag_events']),
        'path_deviation' => $this->calculatePathDeviation($behaviorData['mouse_movements']),
        'acceleration_pattern' => $this->analyzeAccelerationPattern($behaviorData['drag_events']),
        'time_to_react' => $behaviorData['timing']['response_time'],
        'device_consistency' => $this->checkDeviceConsistency($behaviorData)
    ];
    
    // 调用模型API获取评分 (0-1, 越接近1越可能是人类)
    try {
        $response = Http::post(config('services.ml_model.endpoint'), [
            'features' => $features,
            'model_version' => 'captcha_v3'
        ]);
        
        return $response->json()['score'];
    } catch (Exception $e) {
        Log::error('ML model request failed: '.$e->getMessage());
        return 0.5; // 模型不可用时返回中性评分
    }
}

// 示例特征计算函数
protected function calculateSpeedVariance($dragEvents)
{
    if (count($dragEvents) < 2) return 0;
    
    $speeds = [];
    for ($i = 1; $i < count($dragEvents); $i++) {
        $distance = sqrt(
            pow($dragEvents[$i]['x'] - $dragEvents[$i-1]['x'], 2) +
            pow($dragEvents[$i]['y'] - $dragEvents[$i-1]['y'], 2)
        );
        $time = ($dragEvents[$i]['timestamp'] - $dragEvents[$i-1]['timestamp']) / 1000;
        $speeds[] = $time > 0 ? $distance / $time : 0;
    }
    
    return $this->calculateVariance($speeds);
}

七、动态拼图生成算法

1. 高级拼图生成类

class AdvancedJigsawGenerator
{
    private $width = 350;
    private $height = 180;
    private $pieceSize = 50;
    
    public function generate($complexity = 3)
    {
        // 1. 创建背景图
        $bgImage = imagecreatetruecolor($this->width, $this->height);
        $bgColor = imagecolorallocate($bgImage, 245, 245, 245);
        imagefill($bgImage, 0, 0, $bgColor);
        
        // 2. 添加动态纹理背景
        $this->addDynamicTexture($bgImage, $complexity);
        
        // 3. 生成随机拼图位置
        $x = rand(50, $this->width - $this->pieceSize - 20);
        $y = rand(30, $this->height - $this->pieceSize - 20);
        
        // 4. 创建拼图块
        $piece = $this->createJigsawPiece($x, $y, $complexity);
        
        // 5. 在背景上创建缺口
        $this->createGap($bgImage, $x, $y, $piece);
        
        // 6. 添加干扰元素
        $this->addNoise($bgImage, $complexity);
        
        // 7. 生成最终图像
        $token = Str::random(32);
        $position = round(($x / $this->width) * 100, 2); // 存储百分比位置
        
        return [
            'token' => $token,
            'position' => $position,
            'bg_image' => $this->imageToBase64($bgImage),
            'piece_image' => $this->imageToBase64($piece),
            'piece_y' => $y
        ];
    }
    
    private function addDynamicTexture($image, $complexity)
    {
        // 根据复杂度添加不同级别的纹理
        $colors = [
            imagecolorallocate($image, 210, 210, 210),
            imagecolorallocate($image, 220, 220, 220)
        ];
        
        // 随机波浪线
        for ($i = 0; $i < $complexity * 5; $i++) {
            $color = $colors[$i % 2];
            $amplitude = rand(5, 10);
            $frequency = rand(10, 20);
            
            $points = [];
            for ($x = 0; $x < $this->width; $x += 5) {
                $y = $this->height / 2 + sin($x / $frequency) * $amplitude + rand(-5, 5);
                $points[] = $x;
                $points[] = $y;
            }
            
            imagesetthickness($image, rand(1, 2));
            imagepolygon($image, $points, count($points) / 2, $color);
        }
    }
    
    private function createJigsawPiece($x, $y, $complexity)
    {
        $piece = imagecreatetruecolor($this->pieceSize, $this->pieceSize);
        imagesavealpha($piece, true);
        $transparent = imagecolorallocatealpha($piece, 0, 0, 0, 127);
        imagefill($piece, 0, 0, $transparent);
        
        // 随机选择拼图形状
        $shapeType = rand(1, 4);
        $color = imagecolorallocate($piece, rand(50, 150), rand(50, 150), rand(50, 150));
        
        switch ($shapeType) {
            case 1: // 圆形
                imagefilledellipse($piece, $this->pieceSize/2, $this->pieceSize/2, 
                                 $this->pieceSize-2, $this->pieceSize-2, $color);
                break;
            case 2: // 圆角矩形
                $this->drawRoundedRect($piece, 2, 2, $this->pieceSize-4, $this->pieceSize-4, 
                                     5, $color);
                break;
            case 3: // 不规则形状
                $points = $this->generateRandomShape($this->pieceSize, $complexity);
                imagefilledpolygon($piece, $points, count($points)/2, $color);
                break;
            case 4: // 拼图形状
                $this->drawPuzzleShape($piece, $color, $complexity);
                break;
        }
        
        // 添加3D效果
        $this->add3DEffect($piece);
        
        return $piece;
    }
    
    private function drawPuzzleShape($image, $color, $complexity)
    {
        $centerX = $this->pieceSize / 2;
        $centerY = $this->pieceSize / 2;
        
        $points = [];
        $steps = 16 + $complexity * 4; // 复杂度越高边数越多
        
        for ($i = 0; $i < $steps; $i++) {
            $angle = 2 * M_PI * $i / $steps;
            $radius = $this->pieceSize / 2;
            
            // 添加随机凹凸
            if ($i % 2 == 0) {
                $radius += rand(-8, 8) * ($complexity / 3);
            }
            
            $x = $centerX + $radius * cos($angle);
            $y = $centerY + $radius * sin($angle);
            
            $points[] = $x;
            $points[] = $y;
        }
        
        imagefilledpolygon($image, $points, count($points)/2, $color);
    }
    
    private function createGap($bgImage, $x, $y, $piece)
    {
        // 创建与拼图块形状相同的透明区域
        imagecopymerge($bgImage, $piece, $x, $y, 0, 0, 
                      $this->pieceSize, $this->pieceSize, 100);
        
        // 添加边缘高光
        $highlight = imagecolorallocatealpha($bgImage, 255, 255, 255, 60);
        imagerectangle($bgImage, $x-1, $y-1, 
                      $x+$this->pieceSize, $y+$this->pieceSize, $highlight);
    }
    
    private function addNoise($image, $complexity)
    {
        // 随机像素噪声
        for ($i = 0; $i < $this->width * $this->height * $complexity / 100; $i++) {
            $color = imagecolorallocate($image, rand(150, 250), rand(150, 250), rand(150, 250));
            imagesetpixel($image, rand(0, $this->width), rand(0, $this->height), $color);
        }
        
        // 干扰文字
        $fonts = [public_path('fonts/arial.ttf'), public_path('fonts/times.ttf')];
        for ($i = 0; $i < $complexity; $i++) {
            $color = imagecolorallocate($image, rand(180, 220), rand(180, 220), rand(180, 220));
            $angle = rand(-30, 30);
            $fontSize = rand(8, 14);
            $text = substr(str_shuffle('abcdefghijklmnopqrstuvwxyz0123456789'), 0, rand(3, 6));
            
            imagettftext($image, $fontSize, $angle, 
                        rand(0, $this->width-50), rand(0, $this->height-20), 
                        $color, $fonts[array_rand($fonts)], $text);
        }
    }
    
    private function imageToBase64($image)
    {
        ob_start();
        imagepng($image);
        $data = ob_get_clean();
        return 'data:image/png;base64,'.base64_encode($data);
    }
}

八、验证流程安全增强

1. 验证流程时序保护

public function verify(Request $request)
{
    // 1. 检查时间戳防重放攻击
    $timestamp = $request->input('timestamp');
    if (abs(time() - $timestamp) > 60) {
        return response()->json(['error' => '请求已过期'], 401);
    }
    
    // 2. 检查nonce值防重放
    $nonce = $request->input('nonce');
    if (Cache::has('nonce_'.$nonce)) {
        return response()->json(['error' => '重复请求'], 401);
    }
    Cache::put('nonce_'.$nonce, true, now()->addMinutes(5));
    
    // 3. 验证签名
    $signature = $request->input('signature');
    $expected = hash_hmac('sha256', $request->except('signature'), config('app.key'));
    if (!hash_equals($expected, $signature)) {
        return response()->json(['error' => '签名无效'], 401);
    }
    
    // 4. 基础验证
    $token = $request->input('token');
    $position = $request->input('position');
    $captchaData = Cache::get('jigsaw_'.$token);
    
    if (!$captchaData) {
        return response()->json(['error' => '验证码已过期'], 401);
    }
    
    // 5. 行为分析
    $behaviorData = $request->input('behavior');
    $behaviorScore = $this->analyzeBehavior($behaviorData);
    
    // 6. 机器学习评分
    $mlScore = $this->evaluateWithMLModel($behaviorData);
    
    // 7. 综合决策
    $positionTolerance = max(2, 10 - ($behaviorScore + $mlScore) / 2 * 5);
    $positionValid = abs($position - $captchaData['position']) <= $positionTolerance;
    $behaviorValid = ($behaviorScore + $mlScore) / 2 > 0.6;
    
    if ($positionValid && $behaviorValid) {
        // 8. 颁发验证令牌
        $authToken = Str::random(64);
        Cache::put('auth_'.$authToken, [
            'ip' => $request->ip(),
            'user_agent' => $request->userAgent(),
            'expires_at' => now()->addMinutes(15)
        ], now()->addMinutes(15));
        
        return response()->json([
            'success' => true,
            'auth_token' => $authToken
        ]);
    }
    
    // 9. 失败处理
    $this->handleFailedAttempt($request->ip());
    return response()->json(['error' => '验证失败'], 401);
}

2. 验证结果令牌系统

class CaptchaAuth
{
    public static function verifyAuthToken($token)
    {
        $data = Cache::get('auth_'.$token);
        
        if (!$data) {
            return false;
        }
        
        // 检查IP和User-Agent是否匹配
        if ($data['ip'] !== request()->ip() || 
            $data['user_agent'] !== request()->userAgent()) {
            return false;
        }
        
        // 检查是否过期
        if (now()->gt($data['expires_at'])) {
            return false;
        }
        
        // 令牌使用后立即失效
        Cache::forget('auth_'.$token);
        
        return true;
    }
    
    public static function middleware()
    {
        return function ($request, $next) {
            $token = $request->header('X-Captcha-Auth') ?? $request->input('captcha_auth');
            
            if (!self::verifyAuthToken($token)) {
                return response()->json(['error' => '验证令牌无效或已过期'], 401);
            }
            
            return $next($request);
        };
    }
}

九、前端安全增强措施

1. 反调试保护

// 检测开发者工具是否打开
function detectDevTools() {
    const threshold = 160; // 通常打开的开发者工具会使outerWidth减小
    
    function check() {
        if (window.outerWidth - window.innerWidth > threshold || 
            window.outerHeight - window.innerHeight > threshold) {
            document.body.innerHTML = '<h1>请关闭开发者工具后再继续操作</h1>';
            window.location.reload();
        }
    }
    
    setInterval(check, 1000);
    window.addEventListener('resize', check);
}

// 禁用右键菜单和键盘快捷键
document.addEventListener('contextmenu', e => e.preventDefault());
document.addEventListener('keydown', e => {
    if (e.ctrlKey && e.shiftKey && e.key === 'I') e.preventDefault();
    if (e.ctrlKey && e.shiftKey && e.key === 'J') e.preventDefault();
    if (e.ctrlKey && e.key === 'U') e.preventDefault();
});

2. 请求签名

class RequestSigner {
    constructor(secretKey) {
        this.secretKey = secretKey || 'default_secret';
        this.algorithm = 'SHA-256';
    }
    
    /**
     * 生成UUID格式的随机nonce
     * @returns {string} UUID格式的随机字符串
     */
    generateNonce() {
        return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
            const r = Math.random() * 16 | 0;
            const v = c === 'x' ? r : (r & 0x3 | 0x8);
            return v.toString(16);
        });
    }
    
    /**
     * 对对象字段进行规范化处理
     * @param {object} obj - 要处理的对象
     * @returns {object} 处理后的对象
     */
    normalizeObject(obj) {
        const normalized = {};
        
        Object.keys(obj).sort().forEach(key => {
            // 处理嵌套对象
            if (obj[key] && typeof obj[key] === 'object' && !Array.isArray(obj[key])) {
                normalized[key] = this.normalizeObject(obj[key]);
            } 
            // 处理数组
            else if (Array.isArray(obj[key])) {
                normalized[key] = obj[key].map(item => 
                    typeof item === 'object' ? this.normalizeObject(item) : item
                ).sort();
            }
            // 处理基本类型
            else {
                normalized[key] = obj[key];
            }
        });
        
        return normalized;
    }
    
    /**
     * 将对象转换为签名字符串
     * @param {object} obj - 要转换的对象
     * @returns {string} 签名字符串
     */
    objectToSignString(obj) {
        return Object.entries(obj)
            .map(([k, v]) => {
                if (v && typeof v === 'object') {
                    return `${k}=${this.objectToSignString(v)}`;
                } else if (Array.isArray(v)) {
                    return `${k}=[${v.map(item => 
                        typeof item === 'object' ? this.objectToSignString(item) : item
                    ).join(',')}]`;
                } else {
                    return `${k}=${v}`;
                }
            })
            .join('&');
    }
    
    /**
     * 生成HMAC签名
     * @param {string} message - 要签名的消息
     * @returns {Promise<string>} 签名结果
     */
    async generateHMAC(message) {
        try {
            // 创建编码器
            const encoder = new TextEncoder();
            const keyData = encoder.encode(this.secretKey);
            const messageData = encoder.encode(message);
            
            // 导入密钥
            const key = await crypto.subtle.importKey(
                'raw',
                keyData,
                { name: 'HMAC', hash: { name: this.algorithm } },
                false,
                ['sign']
            );
            
            // 生成签名
            const signature = await crypto.subtle.sign(
                'HMAC',
                key,
                messageData
            );
            
            // 转换为十六进制
            const hashArray = Array.from(new Uint8Array(signature));
            return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
        } catch (error) {
            console.error('生成签名失败:', error);
            throw new Error('签名生成失败');
        }
    }
    
    /**
     * 签名请求数据
     * @param {object} data - 要签名的请求数据
     * @returns {Promise<object>} 包含签名后的完整请求数据
     */
    async signRequest(data) {
        const timestamp = Math.floor(Date.now() / 1000);
        const nonce = this.generateNonce();
        
        // 构造基础payload
        const payload = {
            ...data,
            timestamp,
            nonce
        };
        
        // 规范化对象
        const normalized = this.normalizeObject(payload);
        
        // 生成签名字符串
        const strToSign = this.objectToSignString(normalized);
        
        // 生成签名
        const signature = await this.generateHMAC(strToSign);
        
        // 返回签名后的完整请求数据
        return {
            ...payload,
            sign: signature
        };
    }
    
    /**
     * 验证请求签名
     * @param {object} signedData - 已签名的请求数据
     * @returns {Promise<boolean>} 签名是否有效
     */
    async verifySignature(signedData) {
        // 复制数据以避免修改原对象
        const data = { ...signedData };
        const receivedSignature = data.sign;
        delete data.sign;
        
        // 规范化对象
        const normalized = this.normalizeObject(data);
        
        // 生成签名字符串
        const strToSign = this.objectToSignString(normalized);
        
        // 重新生成签名
        const expectedSignature = await this.generateHMAC(strToSign);
        
        // 比较签名
        return receivedSignature === expectedSignature;
    }
}

// 使用示例
(async () => {
    const signer = new RequestSigner('your_secret_key_here');
    
    // 签名请求
    const requestData = {
        userId: 12345,
        action: 'getUserInfo',
        params: {
            fields: ['name', 'email'],
            include: ['profile', 'settings']
        }
    };
    
    try {
        const signedRequest = await signer.signRequest(requestData);
        console.log('签名后的请求:', signedRequest);
        
        // 验证签名
        const isValid = await signer.verifySignature(signedRequest);
        console.log('签名验证结果:', isValid);
    } catch (error) {
        console.error('签名过程中出错:', error);
    }
})();

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐