Symfony2 - 安装了FOS用户软件包的自定义身份验证提供程序

时间:2015-08-13 09:23:04

标签: php symfony authentication fosuserbundle sonata-user-bundle

我需要帮助在Symfony2项目中构建自定义身份验证。我已经阅读了symfony cookbook http://symfony.com/doc/2.3/cookbook/security/custom_authentication_provider.html并发现了很多关于自定义身份验证的问题,但当我尝试使用FOS用户捆绑包时,他们没有回答我的问题。我花了很多时间研究symfony身份验证过程,但无法理解我的错误。

所以我现在拥有:

  1. FOS用户捆绑包已成功安装并配置以下官方文档。
  2. 安装了Sonata用户软件包。
  3. 添加和配置了自定义身份验证侦听器,令牌,提供程序,工厂。 之后我通常会登录,但因为我看到我的新身份验证提供程序未被使用,并且用户已使用FOS" form_login"登录。
  4. 这是我的代码:

    用户实体类:

    <?php
        namespace Acme\UserBundle\Entity;
    
        use Sonata\UserBundle\Entity\BaseUser as BaseUser;
        use Doctrine\ORM\Mapping as ORM;
        use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
        use \Acme\BoardBundle\Entity\Card;
    
        /**
         * @ORM\Entity
         * @ORM\HasLifecycleCallbacks
         * @ORM\Table(name="fos_user")
         */
        class User extends BaseUser
        {
            ...
    
            protected $card;
    
            /**
             * Set card
             *
             * @param \Acme\BoardBundle\Entity\Card $card
             * @return Card
             */
            public function setCard(\Acme\BoardBundle\Entity\Card $card)
            {
                $this->card = $card;
    
                return $this;
            }
    
            /**
             * Get card
             *
             * @return \Acme\BoardBundle\Entity\Card
             */
            public function getCard()
            {
                return $this->card;
            }
        }
    

    User.orm.xml:

    <?xml version="1.0" encoding="UTF-8"?>
    <doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
                      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                      xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
                      http://doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
    
        <entity name="Acme\UserBundle\Entity\User" table="fos_user">
    
            ...
    
            <many-to-one field="card" target-entity="Acme\BoardBundle\Entity\Card" inversed-by="users">
                <join-column name="card" referenced-column-name="id" />
            </many-to-one>
        </entity>
    </doctrine-mapping>
    

    用户实体与卡实体有关系,卡实体有两个属性:卡号和PIN码。我登录后实际需要检查的属性。我的登录表单不仅包含用户名和密码字段,还包含卡号和PIN字段。

    security.yml(我觉得我在防火墙配置方面有一些错误,但我无法理解错误):

    providers:
        fos_userbundle:
            id: fos_user.user_manager
    firewalls:
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false
        admin:
            pattern:            /admin(.*)
            context:            user
            form_login:
                provider:       fos_userbundle
                login_path:     /admin/login
                use_forward:    false
                check_path:     /admin/login_check
                failure_path:   null
            logout:
                path:           /admin/logout
            anonymous:          true
        main:
            pattern:             .*
            context:             user
            acme: true
    
            form_login:
                provider:       fos_userbundle
                login_path:     /user/login
                use_forward:    false
                check_path:     /user/login_check
                failure_path:   null
                always_use_default_target_path: true
                default_target_path:            ad_category
            logout:
                path:           /user/logout
            anonymous:          true
    

    用户令牌:

    <?php
    
    namespace Acme\UserBundle\Security\Authentication\Token;
    
    use Symfony\Component\Security\Core\Authentication\Token\AbstractToken;
    
    class AcmeUserToken extends AbstractToken
    {
        public $userFIO;
        public $cardNumber;
        public $cardPIN;
    
        public function __construct(array $roles = array())
        {
            parent::__construct($roles);
    
            // If the user has roles, consider it authenticated
            $this->setAuthenticated(count($roles) > 0);
        }
    
        public function getCredentials()
        {
            return '';
        }
    
        // поскольку токены проверяются при обработке каждом новом запросе клиента, 
        // нам необходимо сохранять нужные нам данные. В связи с этим “обертываем”  
        // унаследованные методы сериализации и десериализации.
        public function serialize() {        
            $pser = parent::serialize();        
            //return serialize(array($this->social, $this->hash, $this->add, $pser));
            return serialize(array($pser));
        }
    
        public function unserialize($serialized) {
            //list($this->social, $this->hash, $this->add, $pser) = unserialize($serialized);        
            list($pser) = unserialize($serialized);
            parent::unserialize($pser);        
        }
    }
    

    AcmeProvider.php(我的自定义身份验证提供程序):

    <?php
    
    namespace Acme\UserBundle\Security\Authentication\Provider;
    
    use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
    use Symfony\Component\Security\Core\User\UserProviderInterface;
    use Symfony\Component\Security\Core\Exception\AuthenticationException;
    use Symfony\Component\Security\Core\Exception\NonceExpiredException;
    use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
    use Acme\UserBundle\Security\Authentication\Token\AcmeUserToken;
    
    class AcmeProvider implements AuthenticationProviderInterface
    {
        private $userProvider;
    
        public function __construct(UserProviderInterface $userProvider)
        {
            $this->userProvider = $userProvider;
        }
    
        public function authenticate(TokenInterface $token)
        {
            $user = $this->userProvider->loadUserByUsername($token->getUsername());
    
            if ($user) {
                $authenticatedToken = new AcmeUserToken($user->getRoles());
                $authenticatedToken->setUser($user);
    
                return $authenticatedToken;
            }
    
            throw new AuthenticationException('The Acme authentication failed.');
        }
    
        public function supports(TokenInterface $token)
        {
            return $token instanceof AcmeUserToken;
        }
    }
    

    工厂类AcmeFactory.php:

    <?php
    namespace Acme\UserBundle\DependencyInjection\Security\Factory;
    
    use Symfony\Component\DependencyInjection\ContainerBuilder;
    use Symfony\Component\DependencyInjection\Reference;
    use Symfony\Component\DependencyInjection\DefinitionDecorator;
    use Symfony\Component\Config\Definition\Builder\NodeDefinition;
    use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SecurityFactoryInterface;
    
    class AcmeFactory implements SecurityFactoryInterface
    {
        public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
        {
            $providerId = 'security.authentication.provider.acme.'.$id;
            $container
            ->setDefinition($providerId, new DefinitionDecorator('acme.security.authentication.provider'))
            ->replaceArgument(0, new Reference($userProvider))
            ;
    
            $listenerId = 'security.authentication.listener.acme.'.$id;
            $listener = $container->setDefinition($listenerId, new DefinitionDecorator('acme.security.authentication.listener'));
    
            return array($providerId, $listenerId, $defaultEntryPoint);
        }
    
        public function getPosition()
        {
            //return 'pre_auth';
            return 'form';
        }
    
        public function getKey()
        {
            return 'acme';
        }
    
        public function addConfiguration(NodeDefinition $node)
        {
        }
    }
    

    在config.yml中配置用户提供程序和列表器:

    services:
        acme.security.authentication.provider:
            class: Acme\UserBundle\Security\Authentication\Provider\AcmeProvider
            abstract:  true
            arguments: ['']
            public: false
    
        security.authentication.listener.abstract:
            tags:
                - { name: 'monolog.logger', channel: 'security' }
            arguments: [@security.context, @security.authentication.manager, @security.authentication.session_strategy, @security.http_utils, "knetik",@security.authentication.success_handler, @security.authentication.failure_handler, {}, @logger, @event_dispatcher]
            class: Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener
         # override application level success handler and re-route back
        security.authentication.success_handler:
            class:  Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler
            arguments:  ["@security.http_utils", {}]
            tags:
                - { name: 'monolog.logger', channel: 'security' }
        # override application level failure handler and re-route back
        security.authentication.failure_handler:
            class:  Symfony\Component\Security\Http\Authentication\DefaultAuthenticationFailureHandler
            arguments:  ["@http_kernel", "@security.http_utils", {}, "@logger"]
            tags:
                - { name: 'monolog.logger', channel: 'security' }
        yamogu.security.authentication.listener:
            class: Acme\UserBundle\Security\Authentication\Firewall\AcmeListener
            parent: security.authentication.listener.abstract
            abstract:  true
            arguments: ["@security.context", "@security.authentication.manager"]
            public: false
    

    如果您需要附加代码,我会将其添加到问题中。 任何帮助将不胜感激!

    授权后在dev.log上链接:https://www.dropbox.com/s/5uot2qofmqjwvmk/dev.log?dl=0

2 个答案:

答案 0 :(得分:3)

我找到了解决问题的方法,但我采取了另一种方式。 我为form_login定义了一个成功的身份验证处理程序和失败处理程序,并将我的逻辑放在这里。如果用户输入了错误的用户名但是输入了正确的卡号和密码,我会在故障处理程反之亦然,如果用户输入了正确的用户名但错误的卡号和密码,那么我在成功验证失败时拒绝登录并手动注销他。

查看我的源代码

Peace of security.yml:

security:
    firewalls:
        ...
        main:
            pattern:             .*
            context:             user

            form_login:
                provider:       fos_userbundle
                login_path:     /user/login
                use_forward:    false
                check_path:     /user/login_check
                failure_path:   null
                always_use_default_target_path: true
                default_target_path:            ad_category
                success_handler: authentication_success_handler
                failure_handler: authentication_failure_handler
            logout:
                path:           /user/logout
            anonymous:          true

config.yml:

services:
    authentication_success_handler:
        class: Yamogu\UserBundle\Handler\AuthenticationSuccessHandler
        arguments: [@router, @doctrine.orm.entity_manager, @security.context]
    authentication_failure_handler:
        class: Yamogu\UserBundle\Handler\AuthenticationFailureHandler
        arguments: [@router, @doctrine.orm.entity_manager, @security.context, @event_dispatcher]

AuthenticationSuccessHandler.php:

namespace Acme\UserBundle\Handler;

Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
use Doctrine\Common\Persistence\ObjectManager;
use Acme\BoardBundle\Entity\Card;
use Symfony\Component\Security\Core\SecurityContext;

class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    protected $router;

    private $om;

    private $securityContext;

    public function __construct(Router $router, ObjectManager $om, SecurityContext $securityContext)
    {
        $this->router = $router;
        $this->om = $om;
        $this->securityContext = $securityContext;
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token)
    {
        $fosUser = $this->securityContext->getToken()->getUser();
        if($fosUser->getCard())
        {
            $card = $fosUser->getCard()->getNumber();
            $pin = $fosUser->getCard()->getPin();
            if($card == $request->get('card') && $pin == $request->get('pin'))
            { //  if Log out the user he inputs wrong card
                $loginName = $request->get('firstname');
                $fosUserFirstName = $fosUser->getFirstname();
                if($loginName && $loginName != $fosUserFirstName)
                {
                    $fosUser->setFirstname($loginName);
                    $this->om->flush();
                }
                return new RedirectResponse($this->router->generate("ad_category"));
            }
        }
        $this->securityContext->setToken(null);
        $request->getSession()->invalidate();
        $request->getSession()->getFlashBag()->set('acme_login_error', 'Error!');
        return new RedirectResponse($this->router->generate("fos_user_security_login"));
    }
}
?>

AuthenticationFailureHandler.php:

<?php
namespace Acme\UserBundle\Handler;

use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\Routing\Router;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Security\Core\SecurityContext;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Acme\BoardBundle\Entity\Card;
use Acme\UserBundle\Entity\User as YamUser;

class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
    protected $router;
    private $om;
    private $securityContext;
    private $eventDispatcher;

    public function __construct(Router $router, ObjectManager $om, SecurityContext $securityContext, EventDispatcher $eventDispatcher)
    {
        $this->router = $router;
        $this->om = $om;
        $this->securityContext = $securityContext;
        $this->eventDispatcher = $eventDispatcher;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
    {
        if($request->get('firstname') !== null && $request->get('_username') && $request->get('_password') !== null && $request->get('card') !== null && $request->get('pin') !== null)
        {
            $loginName = $request->get('firstname');
            $username = $request->get('_username');
            $passw = $request->get('_password');
            $loginCard = $request->get('card');
            $loginPin = $request->get('pin');
            $card = $this->om->getRepository('AcmeBoardBundle:Card')
                ->findOneBy(array("number" => $loginCard, "pin" => $loginPin));
            // If there is the requested card in the DB create a new user and log in him at the moment
            if($card)
            { // Create a new user for this card, log in him and redirect to the board
                $entity = new YamUser();
                $entity->setCard($card);
                $entity->setFirstname($loginName);
                $entity->setUsername($username);
                $entity->setPlainPassword($passw);
                $entity->setEmail($username);
                $entity->setEnabled(true);
                $this->om->persist($entity);
                $this->om->flush();

                $token = new UsernamePasswordToken($entity, null, "main", $entity->getRoles());
                $this->securityContext->setToken($token); //now the user is logged in
                //now dispatch the login event
                $event = new InteractiveLoginEvent($request, $token);
                $this->eventDispatcher->dispatch("security.interactive_login", $event);
                return new RedirectResponse($this->router->generate("ad_category"));
            }
        }
        $this->securityContext->setToken(null);
        $request->getSession()->invalidate();
        $request->getSession()->getFlashBag()->set('acme_login_error', 'Error!');
        return new RedirectResponse($this->router->generate("fos_user_security_login"));
    }
}
?>

我可以看到这不是解决任务的最佳方式,但它对我有用。如果有人为我的解决方案提供了更好的解决方案或解决方案,请在此处添加!

答案 1 :(得分:0)

您必须让您的安全上下文了解您的捆绑类中的工厂。在您的bundle类中执行以下操作:

class UserBundle extends Bundle
{
    public function build(ContainerBuilder $container)
    {
        parent::build($container);
        $extension = $container->getExtension('security');
        $extension->addSecurityListenerFactory(new AcmeFactory());
    }
    public function getParent()
    {
        return 'FOSUserBundle';
    }
}

<强> [编辑]
Symfony中的安全层很难理解!我建议你按照blog post来了解Symfony的安全性。