本文目录导读:

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