security - password - Symfony2 extendiendo DefaultAuthenticationSuccessHandler
symfony login roles (5)
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 intento 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
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 declaración 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 resto. 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 compilació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 compilador 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 compilació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 compilació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 compilació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 solution .
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 .
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í:
<!-- 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>
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)
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 intento 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.
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);
}
}