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

时间:2022-10-16 13:55:47

I'm working on a Symfony2 application with an API available for other applications. I want to secure the access to the API. For this part I have no problem.

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

But I have to make this connection available not with the usual login/password couple but just with an API key.

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

So I went to the official site and its awesome cookbook for creating a custom authentication provider, just what I need I said to myself.

所以我去了官方网站及其创建自定义身份验证提供程序的精彩菜谱,正如我需要的,我对自己说。

The example was not what I needed but I decided to adapt it to my needs.

这个例子不是我需要的,但我决定让它适应我的需要。

Unfortunately I didn't succeed.

不幸的是我没有成功。

I'll give you my code and I will explain my problem after.

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

Here is my Factory for creating the authentication provider and the listener:

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

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

Next my listener code:

接下来我的听众代码:

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

My authentication provider code:

我的验证提供商代码:

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

To use these two objects I used a yml file to configure them:

要使用这两个对象,我使用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>

Now the authentication provider code:

现在身份验证提供程序代码

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

Just FYI my user provider:

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

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

And for the configuration part my security.yml:

对于配置部分我的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 }

Wow it's a lot of code, I hope it's not too boring.

哇,这是很多代码,我希望它不是太无聊。

My problem here is that my custom authentication provider is called by the two firewalls api and interface instead of just by the api one. And of course they don't behave as I wanted.

我的问题是我的自定义身份验证提供程序由两个防火墙api和接口调用,而不是仅由api调用。当然,他们并没有按照我的意愿行事。

I didn't find anything about such an issue. I know I made a mistake, otherwise it will be working, but where and why I don't know.

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

I also found this tutorial but it didn't help much more.

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

Of course, don't hesitate to suggest me if there is another solution for using another authentication provider than the default one.

当然,如果有另一种使用其他身份验证提供程序的解决方案而不是默认提供程序,请不要犹豫,建议我。

4 个解决方案

#1


10  

So I will answer my own question because I found the solution to my problem and I'll tell you how I solved it.

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

There was some mistake in my example and I understood them searching in the Symfony code.

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

Like the key returned by the getKey method of the Factory class. I found that the api one I've created was for me not an other parameter to my security.yml file, but a replacement to the http_basic one. That's why I'm having some trouble using two providers instead of just one, because I got two keys (api and http_basic) which both used a provider. In fact I think it's the reason to that problem.

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

To make it simple I follow the Symfony tutorial, except for the token class but I replaced the code of the new classes by the code of the Symfony classes. In a kind of way I recreated the http basic authentication of Symfony to make it posssible to overload. And here I am, I could do what I want, configure a different type of http authentication based on the Symfony one but with several changes.

为简单起见,我遵循Symfony教程,除了令牌类,但我用Symfony类的代码替换了新类的代码。我以某种方式重新创建了Symfony的http基本身份验证,使其可以重载。在这里,我可以做我想做的事情,根据Symfony配置不同类型的http身份验证,但有几处更改。

This story helped me because know I know that the best way to understand Symfony principles is to go deeper in the code and look after.

这个故事对我有所帮助,因为我知道了解Symfony原则的最佳方法是深入研究代码并进行管理。

#2


0  

I have found much simpler solution. In config.yml you can point to your custom auth. provider class, like this:

我发现了更简单的解决方案。在config.yml中,您可以指向您的自定义身份验证。提供者类,像这样:

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

Of course MyDaoAuthenticationProvider have to extend Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider

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

#3


0  

I have come upon your problem, and it seems that you did your code well. The thing that could also be causing problems is the order of firewall definitions in security.xml.

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

Try to imagine, if there is some defined Listener(firewall-entry) before your CustomListener and it returns some Response, it will break handlers loop.
Eventually it will cause that your CustomListener is registered, but handle method will never be called.

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

#4


0  

Maybe a little late (5 years later actually), but you have a typo in your Factory. You wrote: $providerId = 'security.authentification.provider.api.'.$id;

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

Where "authentification" has to be authentication

“认证”必须是认证

#1


10  

So I will answer my own question because I found the solution to my problem and I'll tell you how I solved it.

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

There was some mistake in my example and I understood them searching in the Symfony code.

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

Like the key returned by the getKey method of the Factory class. I found that the api one I've created was for me not an other parameter to my security.yml file, but a replacement to the http_basic one. That's why I'm having some trouble using two providers instead of just one, because I got two keys (api and http_basic) which both used a provider. In fact I think it's the reason to that problem.

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

To make it simple I follow the Symfony tutorial, except for the token class but I replaced the code of the new classes by the code of the Symfony classes. In a kind of way I recreated the http basic authentication of Symfony to make it posssible to overload. And here I am, I could do what I want, configure a different type of http authentication based on the Symfony one but with several changes.

为简单起见,我遵循Symfony教程,除了令牌类,但我用Symfony类的代码替换了新类的代码。我以某种方式重新创建了Symfony的http基本身份验证,使其可以重载。在这里,我可以做我想做的事情,根据Symfony配置不同类型的http身份验证,但有几处更改。

This story helped me because know I know that the best way to understand Symfony principles is to go deeper in the code and look after.

这个故事对我有所帮助,因为我知道了解Symfony原则的最佳方法是深入研究代码并进行管理。

#2


0  

I have found much simpler solution. In config.yml you can point to your custom auth. provider class, like this:

我发现了更简单的解决方案。在config.yml中,您可以指向您的自定义身份验证。提供者类,像这样:

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

Of course MyDaoAuthenticationProvider have to extend Symfony\Component\Security\Core\Authentication\Provider\UserAuthenticationProvider

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

#3


0  

I have come upon your problem, and it seems that you did your code well. The thing that could also be causing problems is the order of firewall definitions in security.xml.

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

Try to imagine, if there is some defined Listener(firewall-entry) before your CustomListener and it returns some Response, it will break handlers loop.
Eventually it will cause that your CustomListener is registered, but handle method will never be called.

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

#4


0  

Maybe a little late (5 years later actually), but you have a typo in your Factory. You wrote: $providerId = 'security.authentification.provider.api.'.$id;

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

Where "authentification" has to be authentication

“认证”必须是认证