PHP项目Symfony form与滑块验证

wen PHP项目 2

本文目录导读:

PHP项目Symfony form与滑块验证

  1. 第三方验证服务 (推荐)
  2. 自建滑块验证 (纯 PHP + JavaScript)
  3. 使用第三方库 (更完善)
  4. 总结建议

在 Symfony 项目里集成滑块验证(通常用于防机器人或人机验证),常见的做法是结合前端滑块验证码库与后端校验。

下面是几种主流方案:

第三方验证服务 (推荐)

1 Google reCAPTCHA v2 (I'm not a robot 或 Invisible)

安装 Bundle:

composer require karser/karser-recaptcha-bundle

配置 config/packages/karser_recaptcha.yaml

karser_recaptcha:
    site_key: '%env(GOOGLE_RECAPTCHA_SITE_KEY)%'
    secret_key: '%env(GOOGLE_RECAPTCHA_SECRET_KEY)%'

Form 类型中使用:

use Karser\RecaptchaBundle\Form\Type\RecaptchaType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class ContactType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name')
            ->add('email')
            ->add('recaptcha', RecaptchaType::class, [
                'label' => false,
                'attr' => [
                    'options' => [
                        'theme' => 'light',
                        'size' => 'normal'
                    ]
                ]
            ]);
    }
}

2 hCaptcha (替代方案)

composer require umpirsky/hcaptcha-bundle

配置和使用类似 reCAPTCHA,但更注重隐私。

自建滑块验证 (纯 PHP + JavaScript)

自己实现滑块验证需要后端存储验证状态。

1 滑块验证组件类

// src/Form/Type/SliderCaptchaType.php
namespace App\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\Callback;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
class SliderCaptchaType extends AbstractType
{
    public function buildView(FormView $view, FormInterface $form, array $options): void
    {
        $view->vars['token'] = bin2hex(random_bytes(32));
        // 存储 token 到 session,用于后续验证
        $form->getConfig()->getOption('session')->set('captcha_token_' . $view->vars['token'], [
            'created_at' => time(),
            'verified' => false
        ]);
    }
    public function configureOptions(OptionsResolver $resolver): void
    {
        $resolver->setDefaults([
            'mapped' => false,
            'session' => null,
            'constraints' => [
                new Callback(['callback' => [$this, 'validateCaptcha']])
            ]
        ]);
    }
    public function validateCaptcha($value, ExecutionContextInterface $context): void
    {
        $session = $context->getObject()->getConfig()->getOption('session');
        $stored = $session->get('captcha_token_' . $value);
        if (!$stored || !$stored['verified']) {
            $context->buildViolation('请完成滑块验证')
                ->addViolation();
        }
    }
    public function getParent(): string
    {
        return HiddenType::class;
    }
}

2 在 Form 中使用

// src/Form/ContactType.php
use App\Form\Type\SliderCaptchaType;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
class ContactType extends AbstractType
{
    private $session;
    public function __construct(SessionInterface $session)
    {
        $this->session = $session;
    }
    public function buildForm(FormBuilderInterface $builder, array $options): void
    {
        $builder
            ->add('name')
            ->add('email')
            ->add('message')
            ->add('captcha', SliderCaptchaType::class, [
                'session' => $this->session,
                'label' => false
            ]);
    }
}

3 前端滑块 HTML + JavaScript

{# templates/contact/index.html.twig #}
{% block body %}
    {{ form_start(form) }}
        {{ form_row(form.name) }}
        {{ form_row(form.email) }}
        {{ form_row(form.message) }}
        <div class="slider-captcha">
            <div class="captcha-container" data-token="{{ form.captcha.vars.token }}">
                <div class="captcha-bg"></div>
                <div class="captcha-block" style="left: 0;"></div>
                <div class="captcha-track">
                    <div class="captcha-slider"></div>
                </div>
            </div>
            {{ form_widget(form.captcha) }}
        </div>
        {{ form_row(form.submit) }}
    {{ form_end(form) }}
{% endblock %}
{% block javascripts %}
<script>
document.querySelectorAll('.captcha-container').forEach(container => {
    const slider = container.querySelector('.captcha-slider');
    const block = container.querySelector('.captcha-block');
    const track = container.querySelector('.captcha-track');
    const hiddenInput = container.querySelector('[type="hidden"]');
    let isDragging = false;
    let startX, startLeft;
    let verified = false;
    slider.addEventListener('mousedown', startDrag);
    slider.addEventListener('touchstart', startDrag);
    function startDrag(e) {
        if (verified) return;
        isDragging = true;
        startX = e.clientX || e.touches[0].clientX;
        startLeft = parseInt(block.style.left) || 0;
        document.addEventListener('mousemove', onDrag);
        document.addEventListener('mouseup', stopDrag);
        document.addEventListener('touchmove', onDrag);
        document.addEventListener('touchend', stopDrag);
    }
    function onDrag(e) {
        if (!isDragging) return;
        const currentX = e.clientX || e.touches[0].clientX;
        const diff = currentX - startX;
        let newLeft = startLeft + diff;
        // 限制范围
        if (newLeft < 0) newLeft = 0;
        const maxLeft = track.offsetWidth - block.offsetWidth;
        if (newLeft > maxLeft) newLeft = maxLeft;
        block.style.left = newLeft + 'px';
    }
    function stopDrag(e) {
        if (!isDragging) return;
        isDragging = false;
        document.removeEventListener('mousemove', onDrag);
        document.removeEventListener('mouseup', stopDrag);
        document.removeEventListener('touchmove', onDrag);
        document.removeEventListener('touchend', stopDrag);
        // 验证逻辑:检查是否滑到正确位置
        const maxLeft = track.offsetWidth - block.offsetWidth;
        const threshold = maxLeft * 0.9; // 需要滑动到90%以上
        if (parseInt(block.style.left) >= threshold) {
            verified = true;
            // 通过 AJAX 验证
            fetch('/verify-slider-captcha', {
                method: 'POST',
                headers: {
                    'Content-Type': 'application/json',
                },
                body: JSON.stringify({
                    token: container.dataset.token
                })
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    container.classList.add('verified');
                    hiddenInput.value = container.dataset.token;
                    slider.style.pointerEvents = 'none';
                }
            });
        } else {
            // 复位
            block.style.left = '0px';
        }
    }
});
</script>
{% endblock %}

4 服务器端验证端点

// src/Controller/CaptchaController.php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
class CaptchaController extends AbstractController
{
    #[Route('/verify-slider-captcha', name: 'verify_slider_captcha', methods: ['POST'])]
    public function verifySlider(Request $request): JsonResponse
    {
        $data = json_decode($request->getContent(), true);
        $token = $data['token'] ?? null;
        $session = $this->getParameter('session');
        // 如果使用框架Session
        $stored = $this->get('session')->get('captcha_token_' . $token);
        if ($stored && !$stored['verified']) {
            // 标记已验证
            $this->get('session')->set('captcha_token_' . $token, [
                'created_at' => $stored['created_at'],
                'verified' => true
            ]);
            return $this->json(['success' => true]);
        }
        return $this->json(['success' => false], 400);
    }
}

使用第三方库 (更完善)

推荐 Gregwar/CaptchaBundleCaptchaBundle (friends-of-symfony)

composer require gregwar/captcha-bundle

配置:

# config/packages/gregwar_captcha.yaml
gregwar_captcha:
    length: 5
    width: 200
    height: 50

Form 使用:

use Gregwar\CaptchaBundle\Type\CaptchaType;
$builder->add('captcha', CaptchaType::class, [
    'invalid_message' => '验证码错误',
]);

总结建议

方案 安全性 易用性 维护成本
reCAPTCHA
hCaptcha
自建滑块
文字验证码

对于生产环境,推荐使用第三方验证服务,自建滑块验证虽然可以实现,但需要考虑:

  1. 安全性(容易被自动化绕过)
  2. 用户体验(滑动精度、移动端适配)
  3. 验证逻辑(需要更复杂的校验,比如轨迹分析)

抱歉,评论功能暂时关闭!