在Symfony2中使用自定义身份验证提供程序


Using a custom authentication provider in Symfony2

我正在开发一个Symfony2应用程序,该应用程序具有可用于其他应用程序的API。我想保护对API的访问。对于这部分,我没有问题。

但我必须使这个连接可用,而不是使用通常的登录/密码夫妇,而只是使用API密钥。

所以我去了官方网站和它关于创建自定义身份验证提供商的很棒的食谱,这正是我对自己说的。

这个例子不是我需要的,但我决定根据自己的需要进行调整。

不幸的是,我没有成功。

我会给你我的代码,然后我会解释我的问题。

这是我创建身份验证提供程序和侦听器的工厂:

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

仅供参考我的用户提供商:

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

对于配置部分,我的安全性。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调用。当然,他们并没有按照我的意愿行事。

我没有发现任何关于这样的问题。我知道我犯了一个错误,否则它会起作用,但我不知道在哪里以及为什么。

我也找到了这个教程,但它没有多大帮助。

当然,如果有其他解决方案可以使用默认的身份验证提供程序之外的其他身份验证提供方,请毫不犹豫地向我推荐。

所以我会回答我自己的问题,因为我找到了问题的解决方案,我会告诉你我是如何解决的。

我的例子中有一些错误,我理解他们在Symfony代码中搜索。

类似于Factory类的getKey方法返回的键。我发现我创建的api对我来说不是security.yml文件的另一个参数,而是http_basic文件的替代品。这就是为什么我在使用两个提供者而不是一个提供者时遇到了一些问题,因为我有两个密钥(api和http_basic),它们都使用了一个提供者。事实上,我认为这就是这个问题的原因。

为了简单起见,我遵循了Symfony教程,除了令牌类,但我用Symfony类的代码替换了新类的代码。在某种程度上,我重新创建了Symfony的http基本身份验证,使其可以过载。现在,我可以做我想做的事,基于Symfony配置一种不同类型的http身份验证,但需要进行一些更改。

这个故事帮助了我,因为我知道理解Symfony原则的最好方法是深入代码并加以照顾。

我找到了更简单的解决方案。在config.yml中,您可以指向您的自定义身份验证。提供者类,如下所示:

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

当然MyDaoAuthenticationProvider必须扩展Symfony''Component''Security''Core''Authentication''Provider''UserAuthenticationProvider

我遇到了你的问题,看来你的代码做得很好。也可能导致问题的是security.xml.中防火墙定义的顺序

试着想象一下,如果在CustomListener之前有一些定义的Listener(防火墙条目),并且它返回一些Response,它将中断处理程序循环
最终,它将导致您的CustomListener被注册,但句柄方法永远不会被调用。

也许晚了一点(实际上是5年后),但你的工厂里有一个打字错误。您写道:$providerId = 'security.authentification.provider.api.'.$id;

其中"身份验证"必须是身份验证