2012-01-30 22 views
10

Estoy trabajando en una aplicación Symfony2 con una API disponible para otras aplicaciones. Quiero asegurar el acceso a la API. Para esta parte no tengo ningún problema.Uso de un proveedor de autenticación personalizado en Symfony2

Pero tengo que hacer que esta conexión esté disponible no con el par habitual de inicio de sesión/contraseña, sino solo con una clave API.

Así que fui al sitio oficial y su increíble libro de cocina para creating a custom authentication provider, justo lo que necesito me dije a mí mismo.

El ejemplo no era lo que necesitaba, pero decidí adaptarlo a mis necesidades.

Lamentablemente no tuve éxito.

Le daré mi código y le explicaré mi problema después.

Aquí es mi fábrica para crear el proveedor de autenticación y el oyente:

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

siguiente mi código oyente:

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

Mi código de proveedor de autenticación:

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

Para usar estos dos objetos utilicé un archivo yml para configurarlos:

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

Ahora el código de proveedor de autenticación:

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

Para su información profesional de usuario:

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

Y para la parte de configuración de mi 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 es un montón de código, espero que no sea demasiado aburrido.

Mi problema aquí es que el profesional de la autenticación personalizada es llamado por los dos cortafuegos api y interfaz en lugar de sólo por el api uno. Y, por supuesto, no se comportan como yo quería.

No encontré nada sobre tal problema. Sé que cometí un error, de lo contrario funcionará, pero dónde y por qué no lo sé.

También encontré this tutorial pero no sirvió de mucho más.

Por supuesto, no dude en sugerirme si hay otra solución para utilizar otro proveedor de autenticación distinta a la predeterminada.

+0

¿Por qué necesitan los proveedores de API y la interfaz? Tu ApiProvider no tiene ningún código de autenticación (por ejemplo, comparar la clave pasada por el usuario) –

+0

Porque para la parte de la API de mi aplicación solo quiero que el usuario se autentique con una clave de API, sin importar su contraseña. Si la clave de la API coincide con un usuario, se autentica automáticamente. El proveedor de autenticación predeterminado verifica la contraseña con el nombre de usuario y quiero que se realice esta comprobación. – Maxime

+1

Encontré otra manera de hacer lo que quiero, pero no funcionó. Creé un servicio con "security.authentication.provider.dao.api" como id y dentro de este proveedor de autenticación puse el mismo código que el anterior. Pero tengo el mismo problema, todavía se usa incluso con el firewall de interfaz. Solo quiero sobrecargar Symfony \ Component \ Security \ Core \ Authentication \ Provider \ DaoAuthenticationProvider para evitar la verificación de la contraseña, no debería ser tan complicado, ¿verdad? – Maxime

Respuesta

10

Así que responderé a mi propia pregunta porque encontré la solución a mi problema y le diré cómo lo resolví.

Hubo un error en mi ejemplo y los entendí buscando en el código de Symfony.

Como la clave devuelta por el método getKey de la clase Factory. Descubrí que la API que he creado no era para mí otro parámetro de mi archivo security.yml, sino un reemplazo para http_basic. Es por eso que tengo problemas para utilizar dos proveedores en lugar de uno solo, porque obtuve dos claves (api y http_basic) que utilizan un proveedor. De hecho, creo que es la razón de ese problema.

Para hacerlo simple, sigo el tutorial de Symfony, excepto la clase de token, pero reemplacé el código de las nuevas clases por el código de las clases de Symfony. En cierto modo recreé la autenticación básica http de Symfony para que sea posible sobrecargarla. Y aquí estoy, podría hacer lo que quiera, configurar un tipo diferente de autenticación http basado en Symfony pero con varios cambios.

Esta historia me ayudó porque sé que sé que la mejor manera de entender los principios de Symfony es profundizar en el código y cuidarlo.

+1

marque como respuesta si es la respuesta, por favor –

0

He encontrado una solución mucho más simple. En config.yml puede señalar a su autenticación personalizada. clase de proveedor, así:

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

Por supuesto MyDaoAuthenticationProvider tiene que extenderse Symfony \ Component \ Security \ Core \ autenticación \ Proveedor \ UserAuthenticationProvider

0

He llegado a su problema, y ​​parece que se hizo tu código bien. Lo que también podría estar causando problemas es el orden de las definiciones de firewall en security.xml.

Trate de imaginar, si hay un Listener definido (entrada de firewall) antes de su CustomListener y devuelve algo de Response, romperá el ciclo de los manejadores.
Eventualmente causará que su CustomListener esté registrado, pero nunca se llamará al método handle.

0

Tal vez un poco tarde (5 años más tarde en realidad), pero tiene un error tipográfico en su fábrica. Usted escribió: $providerId = 'security.authentification.provider.api.'.$id;

Donde "autentificación" tiene que ser autenticación

Cuestiones relacionadas