web-dev-qa-db-ja.com

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

_services.yml_にHttpUtilsサービスを追加するのは非常に簡単ですが、オプション引数を使用するとどうなりますか?

_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
_
25
piotrekkr

アプリケーションに定義されている成功/失敗ハンドラーが1つしかない場合、これを行うには少し簡単な方法があります。 success_handlerfailure_handlerの新しいサービスを定義する代わりに、security.authentication.success_handlersecurity.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;
    }
}

私の場合、AJAXを使用して認証しようとしたときにJSON応答を取得できるように何かを設定しようとしていましたが、原理は同じです。

このアプローチの利点は、追加の作業を行わなくても、通常デフォルトのハンドラーに渡されるすべてのオプションが正しく挿入されることです。これは、フレームワークで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_handlersecurity.authentication.failure_handlerを検索して、構成のオプションを渡された配列にマージします。独自のサービスに同様のものを設定する方法は確かにありますが、まだ調べました。

お役に立てば幸いです。

45
dmccabe

このファイルでは、デフォルトのセキュリティリスナーがどのように管理されているかを簡単に確認できます。

vendor/symfony/symfony/src/Symfony/Bundle/SecurityBundle/Resources/config/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では{}で表されると考えてください)

2
Sybio

これまでの最良の解決策については、この回答の一番下までスクロールしてください

OKやっと思い通りに動いた。問題は、カスタムハンドラーが設定されている場合、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はデフォルトハンドラークラスを拡張し、ユーザーパスワードを再ハッシュして、最後にデフォルトハンドラーに残りの処理を任せます。 2つの新しい引数がコンストラクターに追加されました。

#/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からコンストラクターに構成を渡し、さらにコンパイラーパスによって追加された2つの新しい引数を渡します。

より良い方法

セッター付きのサービスとしてのイベントハンドラー

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

}

そして、それはすべて機能しており、コンパイラパスは必要ありません。なぜ最初からそれを考えなかったのか...

うーん、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が solution を書くまで私が知っていた最良のものでした。

2
piotrekkr

残念ながら、セキュリティ構成でsuccess_handlerオプションを使用すると、DefaultAuthenticationSuccessHandlerを拡張するカスタムリスナーを提供できません。

この問題が修正されるまでは symfonyの問題-[2.1] [セキュリティ]カスタムAuthenticationSuccessHandler

それまでは、最も簡単な解決策が@dmccabeの提案です。

グローバルにsecurity.authentication.success_handlerを上書きします。これは、複数のファイアウォールに複数のハンドラーを用意する必要がない限り問題ありません。

(この記事を書いている時点で)行う場合は、 独自の認証プロバイダーを記述する にする必要があります。

1
flu

実際にこれを行う最良の方法は、デフォルトの認証ハンドラをサービスとして拡張することです

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