Symfony2扩展DefaultAuthenticationSuccessHandler
我想在authentication成功之后改变默认的authentication过程。 我做了一个服务,authentication成功后,redirect之前调用。
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
如果您只为应用程序定义了一个成功/失败处理程序,那么执行此操作会稍微简单一些。 而不是为failure_handler
和failure_handler
定义一个新的服务,而是重写security.authentication.success_handler
和security.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进行身份validation时能够获得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
,以便将来自您的configuration的选项合并到传入的数组中。我确定有一种方法可以为您自己的服务设置类似的方法,但是我没有看着它呢。
希望有所帮助。
你可以很容易的看到默认安全监听器在这个文件中是如何pipe理的:
供应商/ symfony中/ symfony中/ src目录/ Symfony的/包/ SecurityBundle /资源/configuration/ 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中的{}表示的)
不幸的是,通过在安全configuration中使用success_handler
选项,您不能提供扩展DefaultAuthenticationSuccessHandler
的自定义侦听器。
直到这个问题得到解决: Symfony的问题 – [2.1] [安全]自定义AuthenticationSuccessHandler
在此之前,最简单的解决scheme是@dmccabe
build议的:
全球覆盖security.authentication.success_handler
这是好的,只要你不需要有多个处理程序的多个防火墙。
如果你这样做(在写这篇文章的时候),你必须编写你自己的authentication提供者 。
到目前为止,最好的解决scheme滚动到这个答案的底部
好的,我终于以我想要的方式工作了。 问题是当自定义处理程序被设置时,Symfony2没有将security.yml
config数组传递给构造函数。 所以我做的是:
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
添加并更改了一些参数,以便我可以在我的编译器pass类中使用它们:
#/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)创build了一个编译器传递类RehashPasswordPass
,它改变了默认的身份validation成功处理程序,并将一些参数添加到构造函数中:
#/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
configuration传递给构造函数,再加上编译器通过添加的两个新的参数。
更好的方法
事件处理程序作为一个服务与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 }
事件处理程序类
# /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(); } } }
它再次运作。
迄今为止最好的方法
上面的解决scheme是我知道最好的,直到@dmccabe写他的解决scheme 。
实际上最好的方法是将默认身份validation处理程序扩展为服务
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); } }