在 symfony 3 登录中实现 recaptcha 的正确方法是什么?
What is the right way to implement recaptcha in symfony 3 login?
我在两种不同的防火墙中使用 form_login,一种用于用户,一种用于管理员。我希望这只影响用户一。
在登录表单上进行重新验证的正确实施方式是什么?
我正在考虑的一些事情:
- 扩展 symfony FormLoginFactory 的新登录表单工厂,我可以在其中验证 recaptcha
或
- 覆盖 UsernamePasswordFormAuthenticationListener 以便 form_login 使用一个新的来验证验证码或
- 将验证码放在自己的页面上,并且仅在用户多次输入无效凭据时才显示
我为这个问题创建了一个包:https://packagist.org/packages/syspay/login-recaptcha-bundle
旧回复:
我是如何解决这个问题的:
我创建了一个名为 CaptchaLoginFormFactory 的新安全侦听器工厂,它具有以下内容
<?php
namespace Project\Bundle\CoreBundle\DependencyInjection\Security\Factory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory;
/**
* CaptchaLoginFormFactory
*/
class CaptchaLoginFormFactory extends FormLoginFactory
{
/**
* {@inheritdoc}
*/
public function getKey()
{
return 'form_login_captcha';
}
/**
* {@inheritdoc}
*/
protected function getListenerId()
{
return 'security.authentication.listener.form_login_captcha';
}
}
和一个名为 CaptchaFormAuthenticationListener 的新身份验证侦听器
<?php
namespace Project\Bundle\CoreBundle\Security\Firewall;
use Project\Security\CaptchaManager;
use Project\Security\Exception\InvalidCaptchaException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener;
use Symfony\Component\Security\Http\ParameterBagUtils;
/**
* CaptchaFormAuthenticationListener
*/
class CaptchaFormAuthenticationListener extends UsernamePasswordFormAuthenticationListener
{
/** @var CaptchaManager $captchaManager */
private $captchaManager;
/**
* setCaptchaManager
*
* @param CaptchaManager $captchaManager
*/
public function setCaptchaManager(CaptchaManager $captchaManager)
{
$this->captchaManager = $captchaManager;
}
/**
* {@inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($this->captchaManager->isCaptchaNeeded($request)) {
$requestBag = $this->options['post_only'] ? $request->request : $request;
$recaptchaResponse = ParameterBagUtils::getParameterBagValue($requestBag, 'g-recaptcha-response');
if (!$this->captchaManager->isValidCaptchaResponse($recaptchaResponse, $request->getClientIp())) {
throw new InvalidCaptchaException();
}
}
return parent::attemptAuthentication($request);
}
}
可以看出,他们对原始 FormFactory 进行了一些更改,在我使用普通身份验证侦听器之前,我使用自己的方法来验证验证码。
然后我在CoreBundle::build方法中添加了
public function build(ContainerBuilder $container)
{
parent::build($container);
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new CaptchaLoginFormFactory());
}
并创建了服务
security.authentication.listener.form_login_captcha:
class: Project\Bundle\CoreBundle\Security\Firewall\CaptchaFormAuthenticationListener
parent: security.authentication.listener.form
abstract: true
calls:
- [ setCaptchaManager, ['@project.security.captcha_manager'] ]
然后在防火墙下的 security.yml 中,我只使用新工厂 form_login_captcha,其选项与 form_login 相同。这样我就可以在另一个防火墙上使用 form_login 而完全不影响它。
这是我在 Symfony 4.4 上使用订阅者使用 https://github.com/karser/KarserRecaptcha3Bundle 的解决方案。这只是一个快速解决方案,您可能需要修改以提高灵活性。
您将需要修改代码以支持在多次尝试失败后显示验证码。
我猜你对不同的防火墙使用了不同的路由。您可以检查登录路由名称并在需要时应用验证码验证。
LoginSubcriber.php:
<?php
namespace App\EventSubscriber;
use App\Form\LoginType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
class LoginSubscriber implements EventSubscriberInterface
{
/**
* @var FormFactoryInterface
*/
private $formFactory;
/**
* @var FlashBagInterface
*/
private $flashBag;
public function __construct(FlashBagInterface $flashBag, FormFactoryInterface $formFactory)
{
$this->formFactory = $formFactory;
$this->flashBag = $flashBag;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
/**
* You can add event subscriber on KernelEvents::REQUEST with priority 9.
* because class Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener(responsible for registering the events for symfony firewall) has priority 8.
*/
return array(
KernelEvents::REQUEST => ['onLogin', 9]
);
}
/**
* @param RequestEvent $event
*/
public function onLogin(RequestEvent $event)
{
if ('public_login' !== $event->getRequest()->attributes->get('_route')) {
return;
}
//form generation should be in the same way (createdNamed in this case) as in LoginController
$loginForm = $this->formFactory->createNamed(null, LoginType::class);
if (!$loginForm->has('captcha')) {
return;
}
$loginForm->handleRequest($event->getRequest());
if (!$loginForm->isSubmitted()) {
return;
}
if (!$loginForm->get('captcha')->isValid()) {
$errors = $loginForm->get('captcha')->getErrors();
$message = count($errors) ? $errors[0]->getMessage() : 'Failed to pass robot test';
$this->flashBag->add(
'error',
$message
);
$session = $event->getRequest()->getSession();
$session->set(Security::LAST_USERNAME, $loginForm->get('_username')->getData());
//to prevent request to call next event
$event->setResponse(new RedirectResponse($event->getRequest()->getRequestUri()));
}
}
}
LoginType.php
<?php
namespace App\Form;
use Karser\Recaptcha3Bundle\Form\Recaptcha3Type;
use Karser\Recaptcha3Bundle\Validator\Constraints\Recaptcha3;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LoginType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username', EmailType::class, ['attr' => ['placeholder' => 'email'], 'data' => $options['lastUsername']])
->add('_password', PasswordType::class, ['attr' => ['placeholder' => 'password']])
->add('_remember_me', CheckboxType::class, ['required' => false])
;
$builder->add('captcha', Recaptcha3Type::class, [
'constraints' => new Recaptcha3(),
'action_name' => 'login'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
//default csrf parameters defined in Symfony codes. without this configuratio csrf check will fail
'csrf_field_name' => '_csrf_token',
'csrf_token_id' => 'authenticate',
'lastUsername' => null
]);
}
}
我在两种不同的防火墙中使用 form_login,一种用于用户,一种用于管理员。我希望这只影响用户一。
在登录表单上进行重新验证的正确实施方式是什么?
我正在考虑的一些事情:
- 扩展 symfony FormLoginFactory 的新登录表单工厂,我可以在其中验证 recaptcha 或
- 覆盖 UsernamePasswordFormAuthenticationListener 以便 form_login 使用一个新的来验证验证码或
- 将验证码放在自己的页面上,并且仅在用户多次输入无效凭据时才显示
我为这个问题创建了一个包:https://packagist.org/packages/syspay/login-recaptcha-bundle
旧回复:
我是如何解决这个问题的:
我创建了一个名为 CaptchaLoginFormFactory 的新安全侦听器工厂,它具有以下内容
<?php
namespace Project\Bundle\CoreBundle\DependencyInjection\Security\Factory;
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory;
/**
* CaptchaLoginFormFactory
*/
class CaptchaLoginFormFactory extends FormLoginFactory
{
/**
* {@inheritdoc}
*/
public function getKey()
{
return 'form_login_captcha';
}
/**
* {@inheritdoc}
*/
protected function getListenerId()
{
return 'security.authentication.listener.form_login_captcha';
}
}
和一个名为 CaptchaFormAuthenticationListener 的新身份验证侦听器
<?php
namespace Project\Bundle\CoreBundle\Security\Firewall;
use Project\Security\CaptchaManager;
use Project\Security\Exception\InvalidCaptchaException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\Firewall\UsernamePasswordFormAuthenticationListener;
use Symfony\Component\Security\Http\ParameterBagUtils;
/**
* CaptchaFormAuthenticationListener
*/
class CaptchaFormAuthenticationListener extends UsernamePasswordFormAuthenticationListener
{
/** @var CaptchaManager $captchaManager */
private $captchaManager;
/**
* setCaptchaManager
*
* @param CaptchaManager $captchaManager
*/
public function setCaptchaManager(CaptchaManager $captchaManager)
{
$this->captchaManager = $captchaManager;
}
/**
* {@inheritdoc}
*/
protected function attemptAuthentication(Request $request)
{
if ($this->captchaManager->isCaptchaNeeded($request)) {
$requestBag = $this->options['post_only'] ? $request->request : $request;
$recaptchaResponse = ParameterBagUtils::getParameterBagValue($requestBag, 'g-recaptcha-response');
if (!$this->captchaManager->isValidCaptchaResponse($recaptchaResponse, $request->getClientIp())) {
throw new InvalidCaptchaException();
}
}
return parent::attemptAuthentication($request);
}
}
可以看出,他们对原始 FormFactory 进行了一些更改,在我使用普通身份验证侦听器之前,我使用自己的方法来验证验证码。
然后我在CoreBundle::build方法中添加了
public function build(ContainerBuilder $container)
{
parent::build($container);
$extension = $container->getExtension('security');
$extension->addSecurityListenerFactory(new CaptchaLoginFormFactory());
}
并创建了服务
security.authentication.listener.form_login_captcha:
class: Project\Bundle\CoreBundle\Security\Firewall\CaptchaFormAuthenticationListener
parent: security.authentication.listener.form
abstract: true
calls:
- [ setCaptchaManager, ['@project.security.captcha_manager'] ]
然后在防火墙下的 security.yml 中,我只使用新工厂 form_login_captcha,其选项与 form_login 相同。这样我就可以在另一个防火墙上使用 form_login 而完全不影响它。
这是我在 Symfony 4.4 上使用订阅者使用 https://github.com/karser/KarserRecaptcha3Bundle 的解决方案。这只是一个快速解决方案,您可能需要修改以提高灵活性。
您将需要修改代码以支持在多次尝试失败后显示验证码。
我猜你对不同的防火墙使用了不同的路由。您可以检查登录路由名称并在需要时应用验证码验证。
LoginSubcriber.php:
<?php
namespace App\EventSubscriber;
use App\Form\LoginType;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Security\Core\Security;
class LoginSubscriber implements EventSubscriberInterface
{
/**
* @var FormFactoryInterface
*/
private $formFactory;
/**
* @var FlashBagInterface
*/
private $flashBag;
public function __construct(FlashBagInterface $flashBag, FormFactoryInterface $formFactory)
{
$this->formFactory = $formFactory;
$this->flashBag = $flashBag;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
/**
* You can add event subscriber on KernelEvents::REQUEST with priority 9.
* because class Symfony\Bundle\SecurityBundle\Debug\TraceableFirewallListener(responsible for registering the events for symfony firewall) has priority 8.
*/
return array(
KernelEvents::REQUEST => ['onLogin', 9]
);
}
/**
* @param RequestEvent $event
*/
public function onLogin(RequestEvent $event)
{
if ('public_login' !== $event->getRequest()->attributes->get('_route')) {
return;
}
//form generation should be in the same way (createdNamed in this case) as in LoginController
$loginForm = $this->formFactory->createNamed(null, LoginType::class);
if (!$loginForm->has('captcha')) {
return;
}
$loginForm->handleRequest($event->getRequest());
if (!$loginForm->isSubmitted()) {
return;
}
if (!$loginForm->get('captcha')->isValid()) {
$errors = $loginForm->get('captcha')->getErrors();
$message = count($errors) ? $errors[0]->getMessage() : 'Failed to pass robot test';
$this->flashBag->add(
'error',
$message
);
$session = $event->getRequest()->getSession();
$session->set(Security::LAST_USERNAME, $loginForm->get('_username')->getData());
//to prevent request to call next event
$event->setResponse(new RedirectResponse($event->getRequest()->getRequestUri()));
}
}
}
LoginType.php
<?php
namespace App\Form;
use Karser\Recaptcha3Bundle\Form\Recaptcha3Type;
use Karser\Recaptcha3Bundle\Validator\Constraints\Recaptcha3;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
class LoginType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('_username', EmailType::class, ['attr' => ['placeholder' => 'email'], 'data' => $options['lastUsername']])
->add('_password', PasswordType::class, ['attr' => ['placeholder' => 'password']])
->add('_remember_me', CheckboxType::class, ['required' => false])
;
$builder->add('captcha', Recaptcha3Type::class, [
'constraints' => new Recaptcha3(),
'action_name' => 'login'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
//default csrf parameters defined in Symfony codes. without this configuratio csrf check will fail
'csrf_field_name' => '_csrf_token',
'csrf_token_id' => 'authenticate',
'lastUsername' => null
]);
}
}