Symfony2 — пользовательский поставщик аутентификации с установленной FOS User Bundle

Мне нужна помощь в создании пользовательской аутентификации в проекте Symfony2. Я прочитал поваренную книгу Symfony http://symfony.com/doc/2.3/cookbook/security/custom_authentication_provider.html и нашел много вопросов о пользовательской аутентификации, но они не ответили на мой вопрос в ситуации, когда я пытался сделать это с помощью FOS User Bundle. Я потратил много времени на изучение процесса аутентификации Symfony, но не могу понять, в чем я не прав.

Итак, что я имею сейчас:

  1. FOS User Bundle успешно установлен и настроен в соответствии с официальной документацией.
  2. Sonata User Bundle установлен.
  3. Пользовательский аутентификатор слушатель, токен, провайдер, фабрика добавлены и настроены.
    После этого я обычно вхожу в систему, но, как я вижу, мой новый поставщик аутентификации не используется, и пользователь входит в систему с помощью form_login FOS.

Вот мой код:

Класс сущности пользователя:

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

Сущность User имеет отношение к сущности Card, которая имеет два свойства: номер карты и 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

Решение

Я нашел решение своей проблемы, но пошел другим путем.
Я определил обработчик успешной аутентификации и обработчик ошибок для form_login и разместил здесь свою логику. Я вручную регистрирую пользователя в обработчике ошибок, если он вводит неправильное имя пользователя, но правильный номер карты и пин-код. И наоборот, если пользователь вводит правильное имя пользователя, но неверный номер карты и пин-код, я отклоняю его логин в случае неудачной аутентификации и вручную выхожу из него.

Смотрите мой исходный код

Мир безопасности.

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"));
}
}
?>

Как я вижу, это не лучший способ решить задачу, но он сработал для меня. Если у кого-то есть лучшее решение или исправления для моего решения, пожалуйста, добавьте их сюда!

3

Другие решения

Вы должны сообщить своему контексту безопасности о своей фабрике в своем классе комплектов. В вашем классе связки сделайте это:

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 очень сложен для понимания! Я предлагаю вам следовать этому Сообщение блога получить представление о безопасности Symfony.

0

По вопросам рекламы [email protected]