Symfony2 extendiendo DefaultAuthenticationSuccessHandler

Quiero modificar el proceso de autenticación predeterminado justo después del éxito de la autenticación. Hice un servicio que se llama después del éxito de autenticación y antes de redirigir.

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 } } 

en 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 

y en 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: / 

Lo que bash lograr es modificar un poco el comportamiento predeterminado, así que creo por qué no extender DefaultAuthenticationSuccessHandler , agregar algo a onSuccessHandler() y llamar a parent::onSucessHandler() . Intenté y el problema es que no tengo idea de cómo agregar parámetros de seguridad (establecidos en security.yml) a mi constructor de clase extendida. DefaultAuthenticationSuccessHandler utiliza HttpUtils y $ options array:

 /** * 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); } 

Entonces mi constructor de clase extendida debería verse así:

  // 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; } 

Es bastante fácil agregar el servicio HttpUtils a my services.yml , pero ¿qué pasa con el argumento 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 

Si solo tiene definido un manejador de éxito / falla para su aplicación, hay una manera un poco más fácil de hacerlo. En lugar de definir un nuevo servicio para success_handler y failure_handler , puede reemplazar security.authentication.success_handler y security.authentication.failure_handler lugar.

Ejemplo:

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

En mi caso, solo estaba tratando de configurar algo para poder obtener una respuesta JSON cuando bash autenticarme usando AJAX, pero el principio es el mismo.

El beneficio de este enfoque es que, sin ningún trabajo adicional, todas las opciones que normalmente pasan a los controladores predeterminados se deben inyectar correctamente. Esto sucede debido a la configuración de SecurityBundle \ DependencyInjection \ Security \ Factory en el marco:

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

Busca específicamente security.authentication.success_handler y security.authentication.failure_handler para fusionar las opciones de su configuración en las matrices que se transfieren. Estoy seguro de que hay una manera de configurar algo similar para su propio servicio, pero no lo he hecho. investigado todavía.

Espero que ayude.

Puede ver fácilmente cómo se gestionan los detectores de seguridad predeterminados en este archivo:

proveedor / symfony / symfony / src / Symfony / Bundle / SecurityBundle / Resources / config / security_listeners.xml

Por ejemplo, DefaultAuthenticationSuccessHandler está registrado así:

   Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler       

Así que finalmente podemos ver que la colección de opciones está vacía por defecto.

options: {} hará el trabajo ^^ (Piensa que una colección es representada por {} en yaml)

Lamentablemente, al utilizar la opción success_handler en la configuración de seguridad, no puede proporcionar un escucha personalizado que extienda DefaultAuthenticationSuccessHandler .

No hasta que se resuelva este problema: problema de Symfony – [2.1] [Seguridad] Custom AuthenticationSuccessHandler

Hasta entonces, la solución más simple es lo que @dmccabe sugirió:

Globalmente sobrescribe el security.authentication.success_handler que está bien siempre que no necesite tener múltiples controladores para múltiples firewalls.

Si lo hace (a partir de este escrito), debe escribir su propio proveedor de autenticación .

Para la mejor solución hasta el momento, vaya al final de esta respuesta

OK, finalmente lo hice funcionar de la manera que quería. El problema era que Symfony2 no pasaba una matriz de configuración de security.yml a constructor cuando se configuraba el manejador personalizado. Entonces lo que hice fue:

1) Eliminé la statement del controlador personalizado de 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 amplía la clase de controlador predeterminado, repite la contraseña de usuario y finalmente permite que el controlador predeterminado haga el rest. Se agregaron dos nuevos argumentos en constructor:

 #/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) agregué y cambié algunos parámetros en my services.yml para poder usarlos en mi clase de pase de comstackción:

 #/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) creó una clase de pase de comstackdor RehashPasswordPass que cambia el controlador de éxito de autenticación predeterminado y agrega algunos parámetros al constructor:

 #/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) paso de comstackción agregado al generador de contenedor:

 #/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()); } } 

Ahora se modificó la clase de controlador predeterminado, pero symfony aún pasará la configuración de security.yml a constructor más dos nuevos argumentos agregados por comstackción.

La mejor manera

Manejador de eventos como servicio con setters

 #/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 } 

Clase de controlador de eventos

 # /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(); } } } 

Y todo está funcionando, no se necesita un pase de comstackción. ¿Por qué no pensé en eso desde el principio …

Uhh dejó de funcionar después de la actualización de Symfony

Ahora tengo una excepción:

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.

Parece que necesito pasar el contenedor completo a mi servicio. Así que modifiqué services.yml y la clase de controlador de eventos.

 #/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 } 

Y controlador de eventos

 # /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(); } } } 

Y funciona de nuevo.

La mejor manera hasta el momento

La solución anterior era lo mejor que sabía hasta que @dmccabe escribió su solución .

en realidad, la mejor manera de hacerlo es ampliar el controlador de autenticación predeterminado como servicio

  authentication_handler: class: AppBundle\Service\AuthenticationHandler calls: [['setDoctrine', ['@doctrine']]] parent: security.authentication.success_handler public: false 

y la clase AuthenticationHandler se vería como

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