本文目录导读:

在 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/CaptchaBundle 或 CaptchaBundle (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 | 高 | 高 | 低 |
| 自建滑块 | 中 | 中 | 高 |
| 文字验证码 | 低 | 中 | 中 |
对于生产环境,推荐使用第三方验证服务,自建滑块验证虽然可以实现,但需要考虑:
- 安全性(容易被自动化绕过)
- 用户体验(滑动精度、移动端适配)
- 验证逻辑(需要更复杂的校验,比如轨迹分析)