Использование специального поставщика проверки подлинности в Symfony2

Я работаю над приложением Symfony2 с API, доступным для других приложений. Я хочу обеспечить доступ к API. Для этой части у меня нет проблем.

Но я должен сделать это соединение доступным не с обычной паролем для входа/пароля, а просто с ключом API.

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

Пример не был тем, что мне нужно, но я решил адаптировать его к моим потребностям.

К сожалению, мне это не удалось.

Я дам вам свой код, и я объясню свою проблему после.

Вот мой Factory для создания поставщика проверки подлинности и слушателя:

<?php

namespace Pmsipilot\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 ApiFactory implements SecurityFactoryInterface
{
  /**
   * @param \Symfony\Component\DependencyInjection\ContainerBuilder $container
   * @param string $id
   * @param aray $config
   * @param string $userProvider
   * @param string $defaultEntryPoint
   * @return array
   */
  public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
  {
    $providerId = 'security.authentification.provider.api.'.$id;
    $container
      ->setDefinition($providerId, new DefinitionDecorator('api.security.authentification.provider'))
      ->replaceArgument(0, new Reference($userProvider))
    ;

    $listenerId = 'security.authentification.listener.api.'.$id;
    $listener = $container->setDefinition($listenerId, new DefinitionDecorator('api.security.authentification.listener'));

    return array($providerId, $listenerId, $defaultEntryPoint);
  }

  /**
   * @return string
   */
  public function getPosition()
  {
    return 'http';
  }

  /**
   * @return string
   */
  public function getKey()
  {
    return 'api';
  }

  /**
   * @param \Symfony\Component\Config\Definition\Builder\NodeDefinition $node
   * @return void
   */
  public function addConfiguration(NodeDefinition $node)
  {
  }
}

Далее мой код слушателя:

<?php

namespace Pmsipilot\UserBundle\Security\Firewall;

use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\Security\Http\Firewall\ListenerInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\SecurityContextInterface;
use Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Pmsipilot\UserBundle\Security\WsseUserToken;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;

class ApiListener implements ListenerInterface
{
  protected $securityContext;
  protected $authenticationManager;

  /**
   * Constructor for listener. The parameters are defined in services.xml.
   *
   * @param \Symfony\Component\Security\Core\SecurityContextInterface $securityContext
   * @param \Symfony\Component\Security\Core\Authentication\AuthenticationManagerInterface $authenticationManager
   */
  public function __construct(SecurityContextInterface $securityContext, AuthenticationManagerInterface $authenticationManager)
  {
    $this->securityContext = $securityContext;
    $this->authenticationManager = $authenticationManager;
  }

  /**
   * Handles login request.
   *
   * @param \Symfony\Component\HttpKernel\Event\GetResponseEvent $event
   * @return void
   */
  public function handle(GetResponseEvent $event)
  {
    $request = $event->getRequest();

    $securityToken = $this->securityContext->getToken();

    if($securityToken instanceof AuthenticationToken)
    {
      try
      {
        $this->securityContext->setToken($this->authenticationManager->authenticate($securityToken));
      }
      catch(\Exception $exception)
      {
        $this->securityContext->setToken(null);
      }
    }
  }
}

Мой код провайдера аутентификации:

<?php

namespace Pmsipilot\UserBundle\Security\Authentication\Provider;

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class ApiProvider implements AuthenticationProviderInterface
{
  private $userProvider;

  /**
   * Constructor.
   *
   * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider An UserProviderInterface instance
   */
  public function __construct(UserProviderInterface $userProvider)
  {
    $this->userProvider = $userProvider;
  }

  /**
   * @param string $username
   * @param \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken $token
   * @return mixed
   * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
   */
  protected function retrieveUser($username, UsernamePasswordToken $token)
  {
    $user = $token->getUser();
    if($user instanceof UserInterface)
    {
      return $user;
    }

    try
    {
      $user = $this->userProvider->loadUserByApiKey($username, $token->getCredentials());

      if(!$user instanceof UserInterface)
      {
        throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
      }

      return $user;
    }
    catch (\Exception $exception)
    {
      throw new AuthenticationServiceException($exception->getMessage(), $token, 0, $exception);
    }
  }

  /**
   * @param TokenInterface $token
   * @return null|\Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
   * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\BadCredentialsException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
   */
  function authenticate(TokenInterface $token)
  {
    $username = $token->getUsername();
    if(empty($username))
    {
      throw new AuthenticationServiceException('No username given.');
    }

    try
    {
      $user = $this->retrieveUser($username, $token);

      if(!$user instanceof UserInterface)
      {
        throw new AuthenticationServiceException('retrieveUser() must return a UserInterface.');
      }

      $authenticatedToken = new UsernamePasswordToken($user, null, 'api', $user->getRoles());
      $authenticatedToken->setAttributes($token->getAttributes());

      return $authenticatedToken;
    }
    catch(\Exception $exception)
    {
      throw $exception;
    }
  }

  /**
   * @param TokenInterface $token
   * @return bool
   */
  public function supports(TokenInterface $token)
  {
    return true;
  }
}

Чтобы использовать эти два объекта, я использовал файл yml для их настройки:

<container xmlns="http://symfony.com/schema/dic/services"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

  <services>
    <service id="pmsipilot.api.security.authentication.factory" class="Pmsipilot\UserBundle\DependencyInjection\Security\Factory\ApiFactory" public="false">
      <tag name="security.listener.factory" />
    </service>
  </services>
</container>

Теперь код поставщика проверки подлинности:

<?php

namespace Pmsipilot\UserBundle\Security\Authentication\Provider;

use Symfony\Component\Security\Core\Authentication\Provider\AuthenticationProviderInterface;
use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\AuthenticationServiceException;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class ApiProvider implements AuthenticationProviderInterface
{
  private $userProvider;

  /**
   * Constructor.
   *
   * @param \Symfony\Component\Security\Core\User\UserProviderInterface $userProvider An UserProviderInterface instance
   */
  public function __construct(UserProviderInterface $userProvider)
  {
    $this->userProvider = $userProvider;
  }

  /**
   * @param string $username
   * @param \Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken $token
   * @return mixed
   * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
   */
  protected function retrieveUser($username, UsernamePasswordToken $token)
  {
    $user = $token->getUser();
    if($user instanceof UserInterface)
    {
      return $user;
    }

    try
    {
      $user = $this->userProvider->loadUserByApiKey($username, $token->getCredentials());

      if(!$user instanceof UserInterface)
      {
        throw new AuthenticationServiceException('The user provider must return a UserInterface object.');
      }

      return $user;
    }
    catch (\Exception $exception)
    {
      throw new AuthenticationServiceException($exception->getMessage(), $token, 0, $exception);
    }
  }

  /**
   * @param TokenInterface $token
   * @return null|\Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken
   * @throws \Symfony\Component\Security\Core\Exception\AuthenticationServiceException|\Symfony\Component\Security\Core\Exception\BadCredentialsException|\Symfony\Component\Security\Core\Exception\UsernameNotFoundException
   */
  function authenticate(TokenInterface $token)
  {
    $username = $token->getUsername();
    if(empty($username))
    {
      throw new AuthenticationServiceException('No username given.');
    }

    try
    {
      $user = $this->retrieveUser($username, $token);

      if(!$user instanceof UserInterface)
      {
        throw new AuthenticationServiceException('retrieveUser() must return a UserInterface.');
      }

      $authenticatedToken = new UsernamePasswordToken($user, null, 'api', $user->getRoles());
      $authenticatedToken->setAttributes($token->getAttributes());

      return $authenticatedToken;
    }
    catch(\Exception $exception)
    {
      throw $exception;
    }
  }

  /**
   * @param TokenInterface $token
   * @return bool
   */
  public function supports(TokenInterface $token)
  {
    return true;
  }
}

Просто FYI, мой провайдер:

<?php

namespace Pmsipilot\UserBundle\Security\Provider;

use Propel\PropelBundle\Security\User\ModelUserProvider;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use \Symfony\Component\Security\Core\Encoder\MessageDigestPasswordEncoder;

class ApiProvider extends ModelUserProvider
{
  /**
   * Constructeur
   */
  public function __construct()
  {
    parent::__construct('Pmsipilot\UserBundle\Model\User', 'Pmsipilot\UserBundle\Proxy\User', 'username');
  }

  /**
   * @param string $apikey
   * @return mixed
   * @throws \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
   */
  public function loadUserByApiKey($apikey)
  {
    $queryClass = $this->queryClass;
    $query      = $queryClass::create();

    $user = $query
      ->filterByApiKey($apikey)
      ->findOne()
    ;

    if(null === $user)
    {
      throw new UsernameNotFoundException(sprintf('User with "%s" api key not found.', $apikey));
    }
    $proxyClass = $this->proxyClass;
    return new $proxyClass($user);
  }
}

И для части конфигурации моя security.yml:

security:
  factories:
    PmsipilotFactory: "%kernel.root_dir%/../src/Pmsipilot/UserBundle/Resources/config/security_factories.xml"

  providers:
    interface_provider:
      id: pmsipilot.security.user.provider
    api_provider:
      id: api.security.user.provider

  encoders:
    Pmsipilot\UserBundle\Proxy\User: sha512

  firewalls:
    assets:
      pattern:                ^/(_(profiler|wdt)|css|images|js|favicon.ico)/
      security:               false

    api:
      provider:               api_provider
      access_denied_url:      /unauthorizedApi
      pattern:                ^/api
      api:                    true
      http_basic:             true
      stateless:              true

    interface:
      provider:               interface_provider
      access_denied_url:      /unauthorized
      pattern:                ^/
      anonymous:              ~
      form_login:
        login_path:           /login
        check_path:           /login_check
        use_forward:          true
        default_target_path:  /
      logout:                 ~

  access_control:
    - { path: ^/api, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: SUPER_ADMIN }

Ничего себе, много кода, я надеюсь, что это не слишком скучно.

Моя проблема заключается в том, что мой собственный поставщик аутентификации вызывается двумя брандмауэрами api и интерфейсом, а не только с помощью api. И, конечно, они не ведут себя так, как я хотел.

Я не нашел ничего подобного. Я знаю, что допустил ошибку, иначе она будет работать, но где и почему я не знаю.

Я также нашел этот учебник, но это не помогло значительно больше.

Конечно, не стесняйтесь предлагать мне, если есть другое решение для использования другого поставщика проверки подлинности, чем стандартное.

Ответ 1

Итак, я отвечу на свой вопрос, потому что нашел решение своей проблемы, и я расскажу вам, как я ее решил.

В моем примере была некоторая ошибка, и я понял, что они ищут код Symfony.

Как и ключ, возвращаемый методом getKey класса Factory. Я обнаружил, что созданный мной api был для меня не другим параметром моего файла security.yml, а заменой http_basic. Вот почему у меня возникают проблемы с использованием двух провайдеров вместо одного, потому что у меня есть два ключа (api и http_basic), которые оба использовали провайдера. На самом деле я считаю это причиной этой проблемы.

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

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

Ответ 2

Я нашел гораздо более простое решение. В config.yml вы можете указать свой собственный auth. класса поставщика, например:

security.authentication.provider.dao.class: App\Security\AuthenticationProvider\MyDaoAuthenticationProvider

Конечно, MyDaoAuthenticationProvider должен расширять Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider

Ответ 3

Я столкнулся с вашей проблемой, и, похоже, вы хорошо сделали свой код. То, что может также вызвать проблемы, - это порядок определений брандмауэра в security.xml.

Попробуйте представить, если перед вашим CustomListener есть некоторый определенный Listener (брандмауэр-запись), и он возвращает некоторый ответ, он прерывает цикл обработчиков.
В конце концов это приведет к регистрации вашего CustomListener, но метод обработки никогда не будет называться.

Ответ 4

Возможно, немного поздно (5 лет спустя на самом деле), но у вас есть опечатка в вашем Factory. Вы написали: $providerId = 'security.authentification.provider.api.'.$id;

Где "аутентификация" должна быть аутентификацией