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

Posted

技术标签:

【中文标题】在 Symfony2 中使用自定义身份验证提供程序【英文标题】:Using a custom authentication provider in Symfony2 【发布时间】:2012-02-22 08:33:20 【问题描述】:

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

但我必须使此连接可用,而不是使用通常的登录名/密码对,而只是使用 API 密钥。

所以我去了官方网站,找到了creating a custom authentication provider 的精彩食谱,我对自己说的正是我需要的。

这个例子不是我需要的,但我决定根据我的需要调整它。

很遗憾我没有成功。

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

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

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

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

哇,这么多代码,希望不会太无聊。

我的问题是我的自定义身份验证提供程序由两个防火墙 apiinterface 调用,而不仅仅是由 api 一个。 当然,他们的行为并不如我所愿。

我没有找到任何关于此类问题的信息。 我知道我犯了一个错误,否则它会起作用,但是我不知道在哪里以及为什么。

我还找到了this tutorial,但它没有更多帮助。

当然,如果有其他解决方案可以使用其他身份验证提供程序而不是默认身份验证提供程序,请随时向我提出建议。

【问题讨论】:

为什么需要api和接口提供者?您的 ApiProvider 没有任何验证码(例如比较用户传递的密钥) 因为对于我的应用程序的 api 部分,我只希望用户使用 api 密钥进行身份验证,无论他的密码如何。如果 api 密钥与用户匹配,则他会自动进行身份验证。默认身份验证提供程序使用用户名检查密码,我希望完成此检查。 我找到了另一种方法来做我想做的事,但它没有用。我创建了一个以“security.authentication.provider.dao.api”为 id 的服务,并在此身份验证提供程序中放置了与上面相同的代码。但是我遇到了同样的问题,即使使用接口防火墙,它仍然可以使用。我只是想重载 Symfony\Component\Security\Core\Authentication\Provider\DaoAuthenticationProvider 来避免密码验证,应该没那么复杂吧? 3 年过去了,情况依旧……即使使用 Knp/Symfony Guard。 【参考方案1】:

可能有点晚了(实际上是 5 年之后),但是您的工厂中有错字。 你写了: $providerId = 'security.authentification.provider.api.'.$id;

“身份验证”必须是身份验证

【讨论】:

【参考方案2】:

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

试想一下,如果在您的 CustomListener 之前有一些定义的 Listener(firewall-entry) 并返回一些响应,它将打破处理程序循环。最终会导致您的 CustomListener 已注册,但处理方法会永远不会被调用。

【讨论】:

【参考方案3】:

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

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

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

【讨论】:

【参考方案4】:

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

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

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

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

这个故事对我很有帮助,因为我知道理解 Symfony 原则的最佳方法是深入研究代码并加以关注。

【讨论】:

以上是关于在 Symfony2 中使用自定义身份验证提供程序的主要内容,如果未能解决你的问题,请参考以下文章

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

Symfony2 实体用户提供者覆盖自定义身份验证提供者

我需要在 symfony2 中使用哪个类进行自定义身份验证

Symfony2 自定义身份验证:用户登录但未通过身份验证

将旧用户迁移到 symfony2

将 Firebase 身份验证与自定义提供程序一起使用