在 symfony 3 登录中实现 recaptcha 的正确方法是什么?

What is the right way to implement recaptcha in symfony 3 login?

我在两种不同的防火墙中使用 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 的解决方案。这只是一个快速解决方案,您可能需要修改以提高灵活性。

您将需要修改代码以支持在多次尝试失败后显示验证码。

我猜你对不同的防火墙使用了不同的路由。您可以检查登录路由名称并在需要时应用验证码验证。

gist code

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
        ]);
    }
}