Laravel 12 实现文字点击验证码功能

Laravel 12 实现文字点击验证码功能

文字点击验证码是一种用户友好且安全性较高的验证方式,用户需要点击图片中指定的文字来完成验证。以下是实现方法:

方法一:使用扩展包(推荐)

1. 安装 inhere/captcha 扩展包

composer require inhere/captcha

2. 创建验证码控制器

namespace App\Http\Controllers;

use Inhere\Captcha\ClickCaptcha;
use Illuminate\Support\Facades\Session;

class CaptchaController extends Controller
{
    public function generate()
    {
        $captcha = new ClickCaptcha([
            'width' => 300,
            'height' => 200,
            'clickCount' => 3, // 需要点击的文字数量
            'fontSize' => 18,
            'fontFile' => public_path('fonts/msyh.ttf') // 字体文件路径
        ]);
        
        $data = $captcha->generate();
        
        // 存储验证数据到session
        Session::put('click_captcha', [
            'code' => $data['code'],
            'points' => $data['points']
        ]);
        
        return response()->json([
            'image' => $data['image'], // base64编码的图片
            'tips' => $data['tips'] // 提示文字如"请点击图中的'苹果'"
        ]);
    }
    
    public function validateCaptcha(Request $request)
    {
        $captchaData = Session::get('click_captcha');
        $userPoints = $request->input('points');
        
        if (!$captchaData || !$this->validatePoints($captchaData['points'], $userPoints)) {
            return response()->json(['success' => false, 'message' => '验证失败']);
        }
        
        Session::forget('click_captcha');
        return response()->json(['success' => true]);
    }
    
    protected function validatePoints($originalPoints, $userPoints, $tolerance = 10)
    {
        if (count($originalPoints) !== count($userPoints)) {
            return false;
        }
        
        foreach ($originalPoints as $i => $point) {
            $distance = sqrt(pow($point['x'] - $userPoints[$i]['x'], 2) + 
                            pow($point['y'] - $userPoints[$i]['y'], 2));
            
            if ($distance > $tolerance) {
                return false;
            }
        }
        
        return true;
    }
}

3. 添加路由

Route::get('/click-captcha/generate', [CaptchaController::class, 'generate']);
Route::post('/click-captcha/validate', [CaptchaController::class, 'validateCaptcha']);

4. 前端实现

<div id="captcha-container">
    <img id="captcha-image" src="" alt="点击验证码">
    <p id="captcha-tips">p>
div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const captchaImage = document.getElementById('captcha-image');
    const captchaTips = document.getElementById('captcha-tips');
    const clickPoints = [];
    
    // 获取验证码
    fetch('/click-captcha/generate')
        .then(response => response.json())
        .then(data => {
            captchaImage.src = data.image;
            captchaTips.textContent = data.tips;
        });
    
    // 点击事件
    captchaImage.addEventListener('click', function(e) {
        const rect = this.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        
        // 添加点击标记
        const marker = document.createElement('div');
        marker.style.position = 'absolute';
        marker.style.left = (x - 5) + 'px';
        marker.style.top = (y - 5) + 'px';
        marker.style.width = '10px';
        marker.style.height = '10px';
        marker.style.backgroundColor = 'red';
        marker.style.borderRadius = '50%';
        this.parentNode.appendChild(marker);
        
        // 记录点击位置
        clickPoints.push({x, y});
        
        // 如果点击次数足够,提交验证
        if (clickPoints.length >= 3) { // 与后端设置的clickCount一致
            fetch('/click-captcha/validate', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                    'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
                },
                body: JSON.stringify({ points: clickPoints })
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    alert('验证成功');
                } else {
                    alert('验证失败,请重试');
                    location.reload();
                }
            });
        }
    });
});
script>

方法二:自定义实现(简单版)

1. 创建验证码生成器

namespace App\Services;

class TextClickCaptcha
{
    public function generate()
    {
        $words = ['苹果', '香蕉', '橙子', '西瓜', '葡萄', '菠萝', '芒果'];
        $selected = array_rand(array_flip($words), 3);
        $target = $selected[array_rand($selected)];
        
        $image = imagecreatetruecolor(300, 200);
        $bgColor = imagecolorallocate($image, 255, 255, 255);
        $textColor = imagecolorallocate($image, 0, 0, 0);
        
        imagefilledrectangle($image, 0, 0, 300, 200, $bgColor);
        
        $positions = [];
        foreach ($selected as $word) {
            $x = rand(20, 250);
            $y = rand(20, 170);
            $positions[$word] = ['x' => $x, 'y' => $y];
            imagettftext($image, 18, 0, $x, $y, $textColor, 
                        public_path('fonts/msyh.ttf'), $word);
        }
        
        ob_start();
        imagepng($image);
        $imageData = ob_get_clean();
        imagedestroy($image);
        
        return [
            'image' => 'data:image/png;base64,' . base64_encode($imageData),
            'target' => $target,
            'positions' => $positions
        ];
    }
}

2. 控制器实现

namespace App\Http\Controllers;

use App\Services\TextClickCaptcha;
use Illuminate\Support\Facades\Session;

class CaptchaController extends Controller
{
    public function generate(TextClickCaptcha $captcha)
    {
        $data = $captcha->generate();
        
        Session::put('text_click_captcha', [
            'target' => $data['target'],
            'positions' => $data['positions']
        ]);
        
        return response()->json([
            'image' => $data['image'],
            'tips' => "请点击图中的'{$data['target']}'"
        ]);
    }
    
    public function validateCaptcha(Request $request)
    {
        $captchaData = Session::get('text_click_captcha');
        $clickPosition = $request->input('position');
        $targetPosition = $captchaData['positions'][$captchaData['target']];
        
        $distance = sqrt(pow($targetPosition['x'] - $clickPosition['x'], 2) + 
                        pow($targetPosition['y'] - $clickPosition['y'], 2));
        
        if ($distance > 20) { // 允许20像素的误差
            return response()->json(['success' => false]);
        }
        
        Session::forget('text_click_captcha');
        return response()->json(['success' => true]);
    }
}

3. 前端实现

<div id="captcha-container">
    <img id="captcha-image" src="" alt="点击验证码">
    <p id="captcha-tips">p>
div>

<script>
document.addEventListener('DOMContentLoaded', function() {
    const captchaImage = document.getElementById('captcha-image');
    const captchaTips = document.getElementById('captcha-tips');
    
    fetch('/click-captcha/generate')
        .then(response => response.json())
        .then(data => {
            captchaImage.src = data.image;
            captchaTips.textContent = data.tips;
            
            captchaImage.addEventListener('click', function(e) {
                const rect = this.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
                
                fetch('/click-captcha/validate', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                        'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content
                    },
                    body: JSON.stringify({ position: {x, y} })
                })
                .then(response => response.json())
                .then(data => {
                    if (data.success) {
                        alert('验证成功');
                    } else {
                        alert('验证失败,请重试');
                        location.reload();
                    }
                });
            });
        });
});
script>

安全增强建议

  1. 限制尝试次数‌:在Session中记录验证失败次数,超过限制则暂时阻止验证
  2. ‌添加时间限制‌:验证码应在生成后5-10分钟内有效
  3. 增加干扰元素‌:在图片中添加干扰线、噪点等
  4. ‌使用加密令牌‌:防止客户端篡改验证数据
  5. 记录验证行为‌:记录IP的验证行为,识别异常模式

样式优化建议

  1. 使用更美观的字体和颜色
  2. 添加点击反馈效果
  3. 实现验证码自动刷新功能
  4. 添加加载状态提示

以上实现可以根据项目需求进行调整,第一种方法使用现成的扩展包更加稳定和安全,第二种方法则提供了更大的灵活性。

你可能感兴趣的:(PHP,经验,Laravel,laravel,php)