Cómo crear un proveedor de usuario personalizado

Parte del proceso de autenticación estándar de Symfony depende del «proveedor de usuarios». Cuando un usuario envía un nombre de usuario y una contraseña, la capa de autenticación solicita al proveedor de usuarios configurado devuelva un objeto usuario para un nombre de usuario dado. Symfony comprueba si la contraseña de este usuario es correcta y genera un indicador de seguridad para que el usuario quede autenticado durante la sesión actual. Fuera de la caja, Symfony tiene un proveedor de usuario «en memoria» y una «entidad». En este artículo varás cómo puedes crear un proveedor de usuarios propio, el cual te podría ser útil si tus usuarios se acceden a través de una base de datos personalizada, un archivo, o —como muestra este ejemplo— un servicio web.

Creando una clase usuario

En primer lugar, independientemente de dónde vengan los datos de tu usuario, tendrás que crear una clase User que represente esos datos. El usuario se puede ver como te apetezca y contener los datos que quieras. El único requisito es que implemente la clase Symfony\Component\Security\Core\User\UserInterface. Los métodos de esta interfaz por lo tanto, se deben definir en la clase usuario personalizada: getRoles(), getPassword(), getSalt(), getUsername(), eraseCredentials(). También puede ser útil implementar la interfaz Symfony\Component\Security\Core\User\EquatableInterface, la cual define un método para comprobar si el usuario es igual al usuario actual. Esta interfaz requiere un método isEqualTo().

Veamos esto en acción:

// src/Acme/WebserviceUserBundle/Security/User/WebserviceUser.php
namespace Acme\WebserviceUserBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

class WebserviceUser implements UserInterface, EquatableInterface
{
    private $username;
    private $password;
    private $salt;
    private $roles;

    public function __construct($username, $password, $salt, array $roles)
    {
        $this->username = $username;
        $this->password = $password;
        $this->salt = $salt;
        $this->roles = $roles;
    }

    public function getRoles()
    {
        return $this->roles;
    }

    public function getPassword()
    {
        return $this->password;
    }

    public function getSalt()
    {
        return $this->salt;
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function eraseCredentials()
    {
    }

    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
                return false;
        }

        if ($this->password !== $user->getPassword()) {
                return false;
        }

        if ($this->getSalt() !== $user->getSalt()) {
                return false;
        }

        if ($this->username !== $user->getUsername()) {
                return false;
        }

            return true;
    }
}

Nuevo en la versión 2.1: La EquatableInterface se añadió en Symfony 2.1. En Symfony 2.0, Usa el método equals() de la UserInterface.

Si tienes más información sobre tus usuarios —como un «nombre»— entonces puedes agregar un campo «first_name» para almacenar esos datos.

Creando un proveedor de usuario

Ahora que tienes una clase Usuario, debes crear un proveedor de usuarios, el cual toma información del usuario desde algún servicio web, crea un objeto WebserviceUser y lo rellena con datos.

El proveedor de usuarios sólo es una simple clase PHP que debe implementar la Symfony\Component\Security\Core\User\UserProviderInterface, la cual requiere que se definan tres métodos: loadUserByUsername($username), refreshUser(UserInterface $user) y supportsClass($class). para más detalles, consulta la Symfony\Component\Security\Core\User\UserProviderInterface.

Aquí tienes un ejemplo de cómo lo podrías hacer:

// src/Acme/WebserviceUserBundle/Security/User/WebserviceUserProvider.php
namespace Acme\WebserviceUserBundle\Security\User;

use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;

class WebserviceUserProvider implements UserProviderInterface
{
    public function loadUserByUsername($username)
    {
        // aquí haz una llamada a tu servicio web
        $userData = ...
        // pretende que devuelve un arreglo cuando tiene éxito,
        // false si no hay usuario

        if ($userData) {
            $password = '...';

            // ...

            return new WebserviceUser($username, $password, $salt, $roles);
        }

        throw new UsernameNotFoundException(sprintf('Username "%s" does not exist.', $username));
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof WebserviceUser) {
            throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user)));
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'Acme\WebserviceUserBundle\Security\User\WebserviceUser';
    }
}

Creando un servicio para el proveedor de usuario

Ahora has disponible el proveedor de usuarios como un servicio:

  • YAML
    # src/Acme/WebserviceUserBundle/Resources/config/services.yml
    parameters:
        webservice_user_provider.class: Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider
    
    services:
        webservice_user_provider:
            class: "%webservice_user_provider.class%"
    
  • XML
    <!-- src/Acme/WebserviceUserBundle/Resources/config/services.xml -->
    <parameters>
        <parameter key="webservice_user_provider.class">Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider</parameter>
    </parameters>
    
        <services>
        <service id="webservice_user_provider" class="%webservice_user_provider.class%"></service>
        </services>
    
  • PHP
    // src/Acme/WebserviceUserBundle/Resources/config/services.php
    use Symfony\Component\DependencyInjection\Definition;
    
    $container->setParameter('webservice_user_provider.class', 'Acme\WebserviceUserBundle\Security\User\WebserviceUserProvider');
    
    $container->setDefinition('webservice_user_provider', new Definition('%webservice_user_provider.class%');
    

Truco

La implementación real del proveedor de usuario probablemente tendrá algunas dependencias u opciones de configuración u otros servicios. Agrégalas como argumentos en la definición del servicio.

Nota

Asegúrate de que estás importando el archivo de servicios. Consulta Importando configuración con imports para más detalles.

Modificando security.yml

Todo viene junto en tu configuración de seguridad. Añade el proveedor de usuario a la lista de proveedores en la sección de «seguridad». Elige un nombre para el proveedor de usuarios (p. ej. «webservice») y menciona el id del servicio que acabas de definir.

  • YAML
    // app/config/security.yml
    security:
        providers:
            webservice:
                id: webservice_user_provider
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <provider name="webservice" id="webservice_user_provider" />
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'providers' => array(
            'webservice' => array(
                'id' => 'webservice_user_provider',
            ),
        ),
    ));
    

Symfony también necesita saber cómo codificar las contraseñas suministradas por los usuarios del sitio web, por ejemplo, rellenando un formulario de acceso. Lo puedes hacer añadiendo una línea a la sección «encoders» en tu configuración de seguridad:

  • YAML
    # app/config/security.yml
    security:
        encoders:
            Acme\WebserviceUserBundle\Security\User\WebserviceUser: sha512
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <encoder class="Acme\WebserviceUserBundle\Security\User\WebserviceUser">sha512</encoder>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'encoders' => array(
            'Acme\WebserviceUserBundle\Security\User\WebserviceUser' => 'sha512',
        ),
    ));
    

El valor aquí debe corresponder con cualquiera de las contraseñas que fueron codificadas originalmente al crear a los usuarios (cuando se crearon los usuarios). cuando un usuario envía su contraseña, se añade el valor de sal a la contraseña y entonces se codifica utilizando el algoritmo antes de comparar la contraseña con algoritmo hash devuelto por el método getPassword(). Además, dependiendo de tus opciones, la contraseña se puede codificar varias veces y codificar a base64.

Bifúrcame en GitHub