Расширение Symfony2 DefaultAuthenticationSuccessHandler
Я хочу изменить процесс аутентификации по умолчанию сразу после успеха проверки подлинности. Я сделал службу, которая вызывается после успешной аутентификации и перед перенаправлением.
namespace Pkr\BlogUserBundle\Handler;
use Doctrine\ORM\EntityManager;
use Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Http\Authentication\Response;
class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
protected $entityManager = null;
protected $logger = null;
protected $encoder = null;
public function __construct(EntityManager $entityManager, LoggerInterface $logger, WpTransitionalEncoder $encoder)
{
$this->entityManager = $entityManager;
$this->logger = $logger;
$this->encoder = $encoder;
}
/**
* This is called when an interactive authentication attempt succeeds. This
* is called by authentication listeners inheriting from
* AbstractAuthenticationListener.
*
* @param Request $request
* @param TokenInterface $token
*
* @return Response never null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$user = $token->getUser();
$newPass = $request->get('_password');
$user->setUserPassword($this->encoder->encodePassword($newPass, null));
$this->entityManager->persist($user);
$this->entityManager->flush();
//do redirect
}
}
в services.yml
services:
pkr_blog_user.wp_transitional_encoder:
class: "%pkr_blog_user.wp_transitional_encoder.class%"
arguments:
cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
logger: @logger
pkr_blog_user.login_success_handler:
class: Pkr\BlogUserBundle\Handler\AuthenticationSuccessHandler
arguments:
entity_manager: @doctrine.orm.entity_manager
logger: @logger
encoder: @pkr_blog_user.wp_transitional_encoder
и в security.yml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: pkr_blog_admin_login
check_path: pkr_blog_admin_login_check
success_handler: pkr_blog_user.login_success_handler
logout:
path: pkr_blog_admin_logout
target: /
То, что я пытаюсь добиться, - просто немного изменить поведение по умолчанию, поэтому я думаю, почему бы не расширить DefaultAuthenticationSuccessHandler
, добавить что-то в onSuccessHandler()
и вызвать parent::onSucessHandler()
. Я попытался, и проблема в том, что я не знаю, как добавить параметры безопасности (установленные в security.yml) в мой расширенный конструктор классов. DefaultAuthenticationSuccessHandler использует массив HttpUtils и $options:
/**
* Constructor.
*
* @param HttpUtils $httpUtils
* @param array $options Options for processing a successful authentication attempt.
*/
public function __construct(HttpUtils $httpUtils, array $options)
{
$this->httpUtils = $httpUtils;
$this->options = array_merge(array(
'always_use_default_target_path' => false,
'default_target_path' => '/',
'login_path' => '/login',
'target_path_parameter' => '_target_path',
'use_referer' => false,
), $options);
}
Итак, мой расширенный конструктор классов должен выглядеть так:
// class extends DefaultAuthenticationSuccessHandler
protected $entityManager = null;
protected $logger = null;
protected $encoder = null;
public function __construct(HttpUtils $httpUtils, array $options, EntityManager $entityManager, LoggerInterface $logger, WpTransitionalEncoder $encoder)
{
$this->entityManager = $entityManager;
$this->logger = $logger;
$this->encoder = $encoder;
}
Довольно легко добавить службу HttpUtils в мой services.yml
, но что с аргументом options?
services:
pkr_blog_user.wp_transitional_encoder:
class: "%pkr_blog_user.wp_transitional_encoder.class%"
arguments:
cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
logger: @logger
pkr_blog_user.login_success_handler:
class: Pkr\BlogUserBundle\Handler\AuthenticationSuccessHandler
arguments:
httputils: @security.http_utils
options: [] #WHAT TO ADD HERE ?
entity_manager: @doctrine.orm.entity_manager
logger: @logger
encoder: @pkr_blog_user.wp_transitional_encoder
Ответы
Ответ 1
Если у вас есть только один обработчик успеха/отказа, определенный для вашего приложения, есть несколько более простой способ сделать это. Вместо определения новой службы для success_handler
и failure_handler
вместо этого вы можете переопределить security.authentication.success_handler
и security.authentication.failure_handler
.
Пример:
services.yml
services:
security.authentication.success_handler:
class: StatSidekick\UserBundle\Handler\AuthenticationSuccessHandler
arguments: ["@security.http_utils", {}]
tags:
- { name: 'monolog.logger', channel: 'security' }
security.authentication.failure_handler:
class: StatSidekick\UserBundle\Handler\AuthenticationFailureHandler
arguments: ["@http_kernel", "@security.http_utils", {}, "@logger"]
tags:
- { name: 'monolog.logger', channel: 'security' }
AuthenticationSuccessHandler.php
<?php
namespace StatSidekick\UserBundle\Handler;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\HttpUtils;
class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler {
public function __construct( HttpUtils $httpUtils, array $options ) {
parent::__construct( $httpUtils, $options );
}
public function onAuthenticationSuccess( Request $request, TokenInterface $token ) {
if( $request->isXmlHttpRequest() ) {
$response = new JsonResponse( array( 'success' => true, 'username' => $token->getUsername() ) );
} else {
$response = parent::onAuthenticationSuccess( $request, $token );
}
return $response;
}
}
AuthenticationFailureHandler.php
<?php
namespace StatSidekick\UserBundle\Handler;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler;
use Symfony\Component\Security\Http\HttpUtils;
class AuthenticationFailureHandler extends DefaultAuthenticationFailureHandler {
public function __construct( HttpKernelInterface $httpKernel, HttpUtils $httpUtils, array $options, LoggerInterface $logger = null ) {
parent::__construct( $httpKernel, $httpUtils, $options, $logger );
}
public function onAuthenticationFailure( Request $request, AuthenticationException $exception ) {
if( $request->isXmlHttpRequest() ) {
$response = new JsonResponse( array( 'success' => false, 'message' => $exception->getMessage() ) );
} else {
$response = parent::onAuthenticationFailure( $request, $exception );
}
return $response;
}
}
В моем случае я просто пытался что-то настроить, чтобы получить ответ JSON при попытке аутентификации с помощью AJAX, но принцип тот же.
Преимущество этого подхода заключается в том, что без какой-либо дополнительной работы все параметры, которые обычно передаются в обработчики по умолчанию, должны вводиться правильно. Это происходит из-за того, как SecurityBundle\DependencyInjection\Security\ Factory настраивается в рамках:
protected function createAuthenticationSuccessHandler($container, $id, $config)
{
...
$successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.success_handler'));
$successHandler->replaceArgument(1, array_intersect_key($config, $this->defaultSuccessHandlerOptions));
...
}
protected function createAuthenticationFailureHandler($container, $id, $config)
{
...
$failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.failure_handler'));
$failureHandler->replaceArgument(2, array_intersect_key($config, $this->defaultFailureHandlerOptions));
...
}
Он специально ищет security.authentication.success_handler
и security.authentication.failure_handler
, чтобы объединить параметры из вашей конфигурации в переданные массивы. Я уверен, что есть способ настроить что-то подобное для вашего собственного сервиса, но я не смотрел в него еще.
Надеюсь, что это поможет.
Ответ 2
Вы можете легко увидеть, как в этом файле работают администраторы безопасности по умолчанию:
продавец/Symfony/Symfony/SRC/Symfony/Bundle/SecurityBundle/Ресурсы/конфигурации/security_listeners.xml
Например, DefaultAuthenticationSuccessHandler зарегистрирован следующим образом:
<!-- Parameter -->
<parameter key="security.authentication.success_handler.class">Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler</parameter>
<!-- Service -->
<service id="security.authentication.success_handler" class="%security.authentication.success_handler.class%" abstract="true" public="false">
<argument type="service" id="security.http_utils" />
<argument type="collection" /> <!-- Options -->
</service>
Итак, наконец, мы видим, что сборка по умолчанию пуста по умолчанию!
options: {}
выполнит задание ^^ (подумайте, что коллекция представляет {} в yaml)
Ответ 3
Для наилучшего решения пока прокрутите до конца этого ответа
ОК, наконец, я получил работу так, как хотел. Проблема заключалась в том, что Symfony2 не передавал конфигурационный массив из security.yml
в конструктор, когда установлен пользовательский обработчик. Так что я сделал:
1) Я удалил объявление пользовательского обработчика из security.yml
firewalls:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false
secured_area:
pattern: ^/
anonymous: ~
form_login:
login_path: pkr_blog_admin_login
check_path: pkr_blog_admin_login_check
logout:
path: pkr_blog_admin_logout
target: /
2) AuthenticationSuccessHandler
расширяет класс обработчика по умолчанию, обновляет пароль пользователя и, наконец, позволяет обработчику по умолчанию делать все остальное. В конструкторе было добавлено два новых аргумента:
#/src/Pkr/BlogUserBundle/Handler/AuthenticationSuccessHandler.php
namespace Pkr\BlogUserBundle\Handler;
use Doctrine\ORM\EntityManager;
use Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Log\LoggerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler;
use Symfony\Component\Security\Http\Authentication\Response;
use Symfony\Component\Security\Http\HttpUtils;
class AuthenticationSuccessHandler extends DefaultAuthenticationSuccessHandler
{
protected $entityManager = null;
protected $logger = null;
protected $encoder = null;
public function __construct(
HttpUtils $httpUtils,
array $options,
// new arguments below
EntityManager $entityManager = null, # entity manager
WpTransitionalEncoder $encoder = null
)
{
$this->entityManager = $entityManager;
$this->encoder = $encoder;
parent::__construct($httpUtils, $options);
}
/**
* This is called when an interactive authentication attempt succeeds. This
* is called by authentication listeners inheriting from
* AbstractAuthenticationListener.
*
* @param Request $request
* @param TokenInterface $token
*
* @return Response never null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
$user = $token->getUser();
if (preg_match('^\$P\$', $user->getUserPassword())) {
$newPass = $request->get('_password');
$user->setUserPassword($this->encoder->encodePassword($newPass, null));
$this->entityManager->persist($user);
$this->entityManager->flush();
}
return parent::onAuthenticationSuccess($request, $token);
}
}
3) добавил и изменил некоторые параметры в моем services.yml
, чтобы я мог использовать их в моем классе пропуска компилятора:
#/src/Pkr/BlogUserBundle/Resources/config/services.yml
parameters:
pkr_blog_user.wp_transitional_encoder.cost: 20
# password encoder class
pkr_blog_user.wp_transitional_encoder.class: Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder
# authentication success handler class
pkr_blog_user.login_success_handler.class: Pkr\BlogUserBundle\Handler\AuthenticationSuccessHandler
# entity manager service name
pkr_blog_user.login_success_handler.arg.entity_manager: doctrine.orm.entity_manager
# encoder service name
pkr_blog_user.login_success_handler.arg.encoder: pkr_blog_user.wp_transitional_encoder
services:
pkr_blog_user.wp_transitional_encoder:
class: "%pkr_blog_user.wp_transitional_encoder.class%"
arguments:
cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
logger: @logger
pkr_blog_user.login_success_handler:
class: "%pkr_blog_user.login_success_handler.class%"
4) создал класс пропуска компилятора RehashPasswordPass
, который изменяет обработчик успешности проверки подлинности по умолчанию и добавляет некоторые параметры конструктору:
#/src/Pkr/BlogUserBundle/DependencyInjection/Compiler/RehashPasswordPass.php
namespace Pkr\BlogUserBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
class RehashPasswordPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
if ($container->hasDefinition('security.authentication.success_handler')) {
// definition of default success handler
$def = $container->getDefinition('security.authentication.success_handler');
// changing default class
$def->setClass($container->getParameter('pkr_blog_user.login_success_handler.class'));
$entityMngRef = new Reference(
$container->getParameter("pkr_blog_user.login_success_handler.arg.entity_manager")
);
// adding entity manager as third param to constructor
$def->addArgument($entityMngRef);
$encoderRef = new Reference(
$container->getParameter("pkr_blog_user.login_success_handler.arg.encoder")
);
// adding encoder as fourth param to constructor
$def->addArgument($encoderRef);
}
}
}
5) добавлен компилятор в конструктор контейнеров:
#/src/Pkr/BlogUserBundle/PkrBlogUserBundle.php
namespace Pkr\BlogUserBundle;
use Pkr\BlogUserBundle\DependencyInjection\Compiler\RehashPasswordPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class PkrBlogUserBundle extends Bundle
{
public function build(ContainerBuilder $container)
{
$container->addCompilerPass(new RehashPasswordPass());
}
}
Теперь класс обработчика по умолчанию был изменен, но symfony по-прежнему передаст конфигурацию из security.yml
в конструктор плюс два новых аргумента, добавленных проходом компилятора.
Лучший способ
Обработчик событий как служба с сеттерами
#/src/Pkr/BlogUserBundle/Resources/config/services.yml
parameters:
pkr_blog_user.wp_transitional_encoder.cost: 15
# password encoder class
pkr_blog_user.wp_transitional_encoder.class: Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder
# authentication success handler class
pkr_blog_user.authentication_success_handler.class: Pkr\BlogUserBundle\EventHandler\AuthenticationSuccessHandler
services:
pkr_blog_user.wp_transitional_encoder:
class: "%pkr_blog_user.wp_transitional_encoder.class%"
arguments:
cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
logger: @logger
pkr_blog_user.authentication_success_handler:
class: "%pkr_blog_user.authentication_success_handler.class%"
calls:
- [ setRequest, [ @request ]]
- [ setEntityManager, [ @doctrine.orm.entity_manager ]]
- [ setEncoder, [ @pkr_blog_user.wp_transitional_encoder ]]
tags:
- { name: kernel.event_listener, event: security.authentication.success , method: handleAuthenticationSuccess }
Класс обработчика событий
# /src/Pkr/BlogUserBundle/EventHandler/AuthenticationSuccessHandler.php
namespace Pkr\BlogUserBundle\EventHandler;
use Doctrine\ORM\EntityManager;
use Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
class AuthenticationSuccessHandler {
protected $entityManager = null;
protected $encoder = null;
public function setRequest(Request $request)
{
$this->request = $request;
}
public function setEntityManager(EntityManager $entityManager)
{
$this->entityManager = $entityManager;
}
public function setEncoder(WpTransitionalEncoder $encoder)
{
$this->encoder = $encoder;
}
public function handleAuthenticationSuccess(AuthenticationEvent $event)
{
$token = $event->getAuthenticationToken();
$user = $token->getUser();
if (preg_match('^\$P\$', $user->getUserPassword())) {
$newPass = $this->request->get('_password');
$user->setUserPassword($this->encoder->encodePassword($newPass, null));
$this->entityManager->persist($user);
$this->entityManager->flush();
}
}
}
И все это работает, компилятор не нужен. Почему я не подумал об этом с самого начала...
Uhh перестала работать после обновления Symfony
Теперь я получаю исключение:
ScopeWideningInjectionException: Scope Widening Injection detected: The definition "pkr_blog_user.authentication_success_handler" references the service "request" which belongs to a narrower scope. Generally, it is safer to either move "pkr_blog_user.authentication_success_handler" to scope "request" or alternatively rely on the provider pattern by injecting the container itself, and requesting the service "request" each time it is needed. In rare, special cases however that might not be necessary, then you can set the reference to strict=false to get rid of this error.
Кажется, мне нужно передать полный контейнер в мою службу. Поэтому я изменил services.yml
и класс обработчика событий.
#/src/Pkr/BlogUserBundle/Resources/config/services.yml
parameters:
pkr_blog_user.wp_transitional_encoder.cost: 15
# password encoder class
pkr_blog_user.wp_transitional_encoder.class: Pkr\BlogUserBundle\Service\Encoder\WpTransitionalEncoder
# authentication success handler class
pkr_blog_user.authentication_success_handler.class: Pkr\BlogUserBundle\EventHandler\AuthenticationSuccessHandler
services:
pkr_blog_user.wp_transitional_encoder:
class: "%pkr_blog_user.wp_transitional_encoder.class%"
arguments:
secure: @security.secure_random
cost: "%pkr_blog_user.wp_transitional_encoder.cost%"
pkr_blog_user.authentication_success_handler:
class: "%pkr_blog_user.authentication_success_handler.class%"
arguments:
container: @service_container
tags:
- { name: kernel.event_listener, event: security.authentication.success , method: handleAuthenticationSuccess }
И обработчик событий
# /src/Pkr/BlogUserBundle/EventHandler/AuthenticationSuccessHandler.php
namespace Pkr\BlogUserBundle\EventHandler;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Security\Core\Event\AuthenticationEvent;
class AuthenticationSuccessHandler
{
/**
* @var ContainerInterface
*/
protected $container;
public function __construct(ContainerInterface $container)
{
$this->container = $container;
}
public function handleAuthenticationSuccess(AuthenticationEvent $event)
{
$request = $this->container->get('request');
$em = $this->container->get('doctrine.orm.entity_manager');
$encoder = $this->container->get('pkr_blog_user.wp_transitional_encoder');
$token = $event->getAuthenticationToken();
$user = $token->getUser();
if (preg_match('/^\$P\$/', $user->getUserPassword())) {
$newPass = $request->get('_password');
$user->setUserPassword($encoder->encodePassword($newPass, null));
$em->persist($user);
$em->flush();
}
}
}
И он работает снова.
Лучший способ пока
Решение выше было лучше всего я знал, пока @dmccabe не написал его .
Ответ 4
К сожалению, используя параметр success_handler
в конфигурации безопасности, вы не можете предоставить пользовательский прослушиватель, который расширяет DefaultAuthenticationSuccessHandler
.
Не исправлена эта проблема: Проблема с Symfony - [2.1] [Безопасность] Пользовательская проверка подлинностиSuccessHandler
До тех пор самое простое решение - это предложение @dmccabe
:
Globaly перезаписывает security.authentication.success_handler
, который отлично до тех пор, пока вам не нужно иметь несколько обработчиков для нескольких брандмауэров.
Если вы это сделаете (начиная с этого письма), вы должны написать собственный поставщик аутентификации.
Ответ 5
на самом деле лучший способ сделать это - расширить обработчик auth по умолчанию как службу
authentication_handler:
class: AppBundle\Service\AuthenticationHandler
calls: [['setDoctrine', ['@doctrine']]]
parent: security.authentication.success_handler
public: false
а класс AuthenticationHandler будет выглядеть как
class AuthenticationHandler extends DefaultAuthenticationSuccessHandler
{
/**
* @var Registry
*/
private $doctrine;
public function setDoctrine(Registry $doctrine)
{
$this->doctrine = $doctrine;
}
/**
* This is called when an interactive authentication attempt succeeds. This
* is called by authentication listeners inheriting from
* AbstractAuthenticationListener.
*
* @param Request $request
* @param TokenInterface $token
*
* @return Response never null
*/
public function onAuthenticationSuccess(Request $request, TokenInterface $token)
{
// do whatever you like here
// ...
// call default success behaviour
return parent::onAuthenticationSuccess($request, $token);
}
}