我在两个不同的防火墙中使用form_login,一个用于用户,一个用于管理员。我希望这只影响用户。
在登录表单上使用 recaptcha 的正确实现是什么?
我正在考虑的一些事情:
- 新的登录表单工厂,它扩展了symfony FormLoginFactory,我可以在其中验证recaptcha或
- 覆盖 UsernamePasswordFormAuthenticationListener,以便form_login使用验证码 或验证码的新侦听器
- 将验证码放在自己的页面上,并且仅在用户多次输入无效凭据时才显示它
我为这个问题创建了一个捆绑包:https://packagist.org/packages/syspay/login-recaptcha-bundle
旧回复:
我做了什么来解决这个问题:
我创建了一个名为 CaptchaLoginFormFactory 的新安全侦听器工厂,它具有以下内容
<?php
namespace ProjectBundleCoreBundleDependencyInjectionSecurityFactory;
use SymfonyBundleSecurityBundleDependencyInjectionSecurityFactoryFormLoginFactory;
/**
* 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 ProjectBundleCoreBundleSecurityFirewall;
use ProjectSecurityCaptchaManager;
use ProjectSecurityExceptionInvalidCaptchaException;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentSecurityHttpFirewallUsernamePasswordFormAuthenticationListener;
use SymfonyComponentSecurityHttpParameterBagUtils;
/**
* 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: ProjectBundleCoreBundleSecurityFirewallCaptchaFormAuthenticationListener
parent: security.authentication.listener.form
abstract: true
calls:
- [ setCaptchaManager, ['@project.security.captcha_manager'] ]
然后在防火墙下的security.yml中,我只使用与form_login相同的选项的新工厂form_login_captcha。这样,我可以在另一个防火墙上使用form_login而不会影响它。
在Symfony 4.4上使用订阅者使用 https://github.com/karser/KarserRecaptcha3Bundle 的解决方案。这只是一个快速的解决方案,您可能需要修改以提高灵活性。
您需要修改代码以支持在多次失败尝试后显示验证码。
我猜您正在为不同的防火墙使用不同的路由。您可以检查登录路由名称并根据需要应用验证码验证。
要点代码
登录订阅者.php:
<?php
namespace AppEventSubscriber;
use AppFormLoginType;
use SymfonyComponentEventDispatcherEventSubscriberInterface;
use SymfonyComponentFormFormFactoryInterface;
use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentHttpFoundationSessionFlashFlashBagInterface;
use SymfonyComponentHttpKernelEventRequestEvent;
use SymfonyComponentHttpKernelKernelEvents;
use SymfonyComponentSecurityCoreSecurity;
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 SymfonyBundleSecurityBundleDebugTraceableFirewallListener(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()));
}
}
}
登录类型.php
<?php
namespace AppForm;
use KarserRecaptcha3BundleFormRecaptcha3Type;
use KarserRecaptcha3BundleValidatorConstraintsRecaptcha3;
use SymfonyComponentFormAbstractType;
use SymfonyComponentFormExtensionCoreTypeCheckboxType;
use SymfonyComponentFormExtensionCoreTypeEmailType;
use SymfonyComponentFormExtensionCoreTypePasswordType;
use SymfonyComponentFormFormBuilderInterface;
use SymfonyComponentOptionsResolverOptionsResolver;
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
]);
}
}