Symfony2 extendingfault authentications Successfulhandler

je veux modifier le processus d'authentification par défaut juste après le succès de l'authentification. J'ai créé un service qui est appelé après le succès de l'authentification et avant la redirection.

namespace PkrBlogUserBundleHandler;
use DoctrineORMEntityManager;
use PkrBlogUserBundleServiceEncoderWpTransitionalEncoder;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpKernelLogLoggerInterface;
use SymfonyComponentSecurityCoreAuthenticationTokenTokenInterface;
use SymfonyComponentSecurityHttpAuthenticationAuthenticationSuccessHandlerInterface;
use SymfonyComponentSecurityHttpAuthenticationResponse;

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

dans 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: PkrBlogUserBundleHandlerAuthenticationSuccessHandler
        arguments:
            entity_manager: @doctrine.orm.entity_manager
            logger: @logger
            encoder: @pkr_blog_user.wp_transitional_encoder

et en sécurité.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: /

ce que j'essaie de réaliser est de juste modifier le comportement par défaut un peu alors je pense pourquoi ne pas étendre DefaultAuthenticationSuccessHandler , ajouter quelque chose à onSuccessHandler() et appeler parent::onSucessHandler() . J'ai essayé et le problème est que je n'ai aucune idée comment ajouter des paramètres de sécurité (défini dans la sécurité.yml) à mon constructeur de classe étendue. Defaultauthenticationsucchandler utilise le tableau HttpUtils et $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);
}

donc mon constructeur de classe étendue devrait ressembler à:

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

il est assez facile d'ajouter le service HttpUtils à mon services.yml , mais qu'en est-il de l'argument 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: PkrBlogUserBundleHandlerAuthenticationSuccessHandler
        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
25
demandé sur piotrekkr 2013-04-10 10:34:18

5 réponses

si vous n'avez qu'un handler succès / échec défini pour votre application, il y a une façon un peu plus facile de le faire. Plutôt que de définir un nouveau service pour les success_handler et failure_handler , vous pouvez remplacer security.authentication.success_handler et security.authentication.failure_handler à la place.

exemple:

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

Authentic Successfulhandler.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;
    }
}

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

dans mon cas, j'essayais juste de configurer quelque chose pour que je puisse obtenir une réponse JSON quand j'essaie d'authentifier en utilisant AJAX, mais le principe est le même.

l'avantage de cette approche est que sans aucun travail supplémentaire, toutes les options qui sont normalement passées dans les gestionnaires par défaut devraient être injectées correctement. Cela arrive parce que de la façon dont SecurityBundle\DependencyInjection\Security\Factory est installé dans le cadre:

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

il recherche spécifiquement security.authentication.success_handler et security.authentication.failure_handler afin de fusionner les options de votre configuration dans les tableaux passés. Je suis sûr qu'il y a un moyen de mettre en place quelque chose de similaire pour votre propre service, mais je ne l'ai pas encore examiné.

Espère que ça aide.

44
répondu dmccabe 2016-11-10 21:28:39

vous pouvez facilement voir comment les auditeurs de sécurité par défaut sont gérés dans ce fichier:

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

par exemple,

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

donc finalement nous pouvons voir que la collection d'Options est vide par défaut !

options: {} la collection est représenter par {} en yaml)

2
répondu Sybio 2013-04-10 07:46:18

malheureusement, en utilisant l'option success_handler dans la configuration de sécurité, vous ne pouvez pas fournir un écouteur personnalisé qui étend DefaultAuthenticationSuccessHandler .

Pas jusqu'à ce que ce problème est résolu: Symfony question - [2.1][Sécurité] Personnalisé AuthenticationSuccessHandler

D'ici là, la solution la plus simple est ce que @dmccabe suggère:

écrase Globaly le security.authentication.success_handler ce qui est très bien tant que vous ne besoin d'avoir plusieurs gestionnaires pour plusieurs pare-feu.

si vous le faites (à partir de cet écrit-) vous devez écrire votre propre fournisseur D'authentification .

1
répondu flu 2014-03-13 12:27:37

Pour la meilleure solution jusqu'ici défiler vers le bas de cette réponse

OK j'ai finalement obtenu que ça fonctionne comme je le voulais. Le problème était que Symfony2 ne passait pas le tableau de configuration de security.yml au constructeur lorsque le gestionnaire personnalisé est défini. Alors ce que j'ai fait était:

1) j'ai supprimé le gestionnaire personnalisé déclaration 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 étend la classe de gestionnaire par défaut, le mot de passe utilisateur de rehash et enfin, laissez le handler par défaut faire le reste. Deux nouveaux arguments ont été ajoutés dans le constructeur:

#/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) Ajouté et modifié certains paramètres dans mon services.yml pour que je puisse les utiliser dans ma classe de passe de compilateur:

#/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) créé une classe de réussite de compilateur RehashPasswordPass qui modifie le gestionnaire de succès d'authentification par défaut et ajoute quelques paramètres au constructeur:

#/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) passage du compilateur ajouté au conteneur constructeur:

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

maintenant la classe de handler par défaut a été changée mais symfony passera encore la configuration de security.yml au constructeur plus deux nouveaux arguments ajoutés par le compilateur pass.

La meilleure façon

gestionnaire d'Événement en tant que service avec les poseurs

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

gestionnaire d'Événement de la classe

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

}

et tout fonctionne, pas besoin de passer compilateur. Pourquoi n'ai-je pas pensé à cela depuis le début...

Heu ... il a arrêté de travailler après la mise à jour symfony

Maintenant, je reçois exception:

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.

il semble que j'ai besoin de passer conteneur complet à mon service. J'ai donc modifié services.yml et la classe du gestionnaire d'événements.

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

et le gestionnaire d'événements

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

}

et ça marche à nouveau.

Meilleur moyen à ce jour

la solution ci-dessus était la meilleure que je savais jusqu'à ce que @dmccabe a écrit son solution .

1
répondu piotrekkr 2017-05-23 11:47:08

en fait, la meilleure façon de faire ceci est d'étendre le handler authler par défaut comme service

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

et la classe D'Authentificationhandler ressemblerait à

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);
    }
}
0
répondu Bogdans 2016-08-08 23:34:20