Symfony2 扩展 DefaultAuthenticationSuccessHandler
Posted
技术标签:
【中文标题】Symfony2 扩展 DefaultAuthenticationSuccessHandler【英文标题】:Symfony2 extending DefaultAuthenticationSuccessHandler 【发布时间】:2013-04-01 20:49:15 【问题描述】:我想在身份验证成功后更改默认身份验证过程。我创建了一个在身份验证成功后重定向之前调用的服务。
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
很容易,但是选项参数呢?
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】:如果您只为您的应用程序定义了一个成功/失败处理程序,那么有一种更简单的方法可以做到这一点。您可以覆盖security.authentication.success_handler
和security.authentication.failure_handler
,而不是为success_handler
和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_handler
和 security.authentication.failure_handler
以便将您的配置中的选项合并到传入的数组中。我确信有一种方法可以为您自己的服务设置类似的东西,但我没有看过进入它。
希望对您有所帮助。
【讨论】:
谢谢,看来你的想法比我的好。不知道我可以覆盖我的services.yml
中的其他捆绑服务配置,但现在我知道了。谢谢
有没有办法在处理程序中重定向而不会出现错误“尝试在类 Site\MyBundle\Handler\LoginSuccessHandler 上调用方法“重定向””
您是如何尝试进行重定向的?这些处理程序上没有重定向方法,因此您需要返回一个新的 RedirectResponse。
您刚刚描述了我自己的问题的解决方案。欣赏!
自 Symfony 2.8 以来,有一个 Guard Authenticator Service
用于简化创建自定义身份验证系统。有onAuthenticationSuccess()
方法可以返回RedirectResponse
。在此处查看教程sitepoint.com/easier-authentication-with-guard-in-symfony-3【参考方案2】:
实际上最好的方法是将默认身份验证处理程序扩展为服务
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);
【讨论】:
【参考方案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
传递给构造函数以及编译器传递添加的两个新参数。
更好的方法
事件处理程序作为带有 setter 的服务
#/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 之前,我知道上面的解决方案是最好的。
【讨论】:
注入@request_stack
而不是整个容器呢?稍后您可以使用$requestStack->getCurrentRequest()
检索当前请求对象。这个cookbook page 还包含一些关于范围和服务容器的信息。
@PaulT.Rawkeen 你是对的,我可能可以将我需要的一切作为参数或方法调用传递给我的服务,而不使用完整的容器,但是当我写下我的答案时,我知道的不多Symfony 内部结构。今天我将编写自己的身份验证提供程序,如下所示:symfony.com/doc/current/cookbook/security/…
刚刚提到过,因为最好记住注入整个容器 不是一个好习惯。顺便说一句,我们都不太擅长 Symfony 内部结构 :)。【参考方案4】:
很遗憾,通过在安全配置中使用 success_handler
选项,您无法提供扩展 DefaultAuthenticationSuccessHandler
的自定义侦听器。
直到这个问题得到解决:Symfony issue - [2.1][Security] Custom AuthenticationSuccessHandler
在此之前,最简单的解决方案是@dmccabe
建议的:
Globaly 覆盖 security.authentication.success_handler
,只要您不需要为多个防火墙设置多个处理程序就可以了。
如果你这样做(在撰写本文时),你必须write your own Authentication Provider。
【讨论】:
【参考方案5】:您可以在此文件中轻松查看默认安全侦听器的管理方式:
供应商/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 中的 表示)
【讨论】:
是的,这也是之前看到的,但这是否意味着它总是空的?如果我在security.yml
中为login_form
设置一些选项(如login_path
或target_path_parameter
)会怎样?这个选项还是空的吗?
我怀疑在使用来自security.yml
的自定义成功处理程序选项时未设置:(以上是关于Symfony2 扩展 DefaultAuthenticationSuccessHandler的主要内容,如果未能解决你的问题,请参考以下文章
Symfony2 使用父级中的一个字段和具有不同注释/映射的扩展实体