Ver índice de contenidos del libro

13.5. Usuarios

En las secciones anteriores, se explicó cómo utilizar los roles para proteger diferentes recursos. En esta sección se va a explorar la otra parte de la autorización: los usuarios.

13.5.1. ¿De dónde provienen los usuarios?

Durante la autenticación, el usuario envía un conjunto de credenciales (por lo general un nombre de usuario y contraseña). El trabajo del sistema de autenticación es verificar esas credenciales utilizando la lista completa de usuarios de la aplicación. La pregunta es, ¿de dónde provienen los usuarios de esa lista?

En Symfony2, los usuarios pueden venir de varios sitios diferentes: un archivo de configuración, una tabla de base de datos, un servicio web, o cualquier otra cosa que se te ocurra. Cualquier elemento que proporcione uno o más usuarios al sistema de autenticación se conoce como proveedor de usuarios.

Symfony2 incluye de serie los dos proveedores de usuarios más comunes: uno que carga los usuarios de un archivo de configuración y otro que carga usuarios de una tabla de la base de datos.

13.5.1.1. Definiendo usuarios en un archivo de configuración

La forma más fácil para especificar los usuarios de la aplicación consiste en utilizar directamente un archivo de configuración. De hecho, esto es lo que ya has visto en los ejemplos de las secciones anteriores.

# app/config/security.yml
security:
    # ...
    providers:
        default_provider:
            memory:
                users:
                    ryan:  { password: ryanpass, roles: 'ROLE_USER' }
                    admin: { password: kitten, roles: 'ROLE_ADMIN' }
<!-- app/config/security.xml -->
<config>
    <!-- ... -->
    <provider name="default_provider">
        <memory>
            <user name="ryan" password="ryanpass" roles="ROLE_USER" />
            <user name="admin" password="kitten" roles="ROLE_ADMIN" />
        </memory>
    </provider>
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    ...,
    'providers' => array(
        'default_provider' => array(
            'memory' => array(
                'users' => array(
                    'ryan'  => array('password' => 'ryanpass', 'roles' => 'ROLE_USER'),
                    'admin' => array('password' => 'kitten', 'roles'   => 'ROLE_ADMIN'),
                ),
            ),
        ),
    ),
));

Este proveedor se denomina roveedor de usuarios en memoria, ya que los usuarios no se almacenan en ninguna base de datos. El objeto que representa realmente al usuario lo proporciona Symfony a través de la clase User.

Truco Cualquier proveedor de usuarios puede cargar usuarios directamente desde un archivo simplemente especificando la opción de configuración users y después la lista de usuarios.

Advertencia Si el nombre de algún usuario está formado exclusivamente por números (por ejemplo, 77) o contiene un guión medio (por ejemplo, user-name), debes utilizar la sintaxis alternativa al especificar los usuarios en el archivo YAML:

users:
    - { name: 77, password: pass, roles: 'ROLE_USER' }
    - { name: user-name, password: pass, roles: 'ROLE_USER' }

Para sitios pequeños, este método es rápido y fácil de configurar. Para aplicaciones más complejas, será mejor que cargues los usuarios desde la base de datos.

13.5.1.2. Cargando usuarios desde la base de datos

Si quieres cargar tus usuarios a través del ORM de Doctrine, lo puedes hacer creando una clase User y configurando el proveedor entity.

Truco FOSUserBundle es un bundle de alta calidad y publicado como software libre que te permite almacenar tus usuarios utilizando el ORM o el ODM de Doctrine.

Así que primero tienes que crear tu propia clase de tipo User, para después almacenar sus datos en la base de datos.

// src/Acme/UserBundle/Entity/User.php
namespace Acme\UserBundle\Entity;
 
use Symfony\Component\Security\Core\User\UserInterface;
use Doctrine\ORM\Mapping as ORM;
 
/**
 * @ORM\Entity
 */
class User implements UserInterface
{
    /**
     * @ORM\Column(type="string", length=255)
     */
    protected $username;
 
    // ...
}

Por lo que respecta al sistema de seguridad, el único requisito que debe cumplir la clase es que implemente la interfaz UserInterface. Esto significa que el concepto que tu tengas de lo que es un usuario puede ser cualquier cosa, siempre y cuando implemente esta interfaz.

A partir de Symfony 2.1, ya no está disponible el método equals en la interfaz UserInterface. Si necesitas redefinir la lógica que se utiliza por defecto para comparar usuarios, debes implementar la nueva interfaz EquatableInterface.

Nota El objeto que representa al usuario se serializa y se guarda en la sesión de una petición a otra. Así que es recomendable que tu clase implemente la interfaz Serializable. Esto es sobre todo necesario cuando la clase de usuario extiende de otra clase que tiene propiedades privadas.

A continuación, configura un proveedor de usuarios de tipo entity y haz que apunte a la clase User que acabas de definir:

# app/config/security.yml
security:
    providers:
        main:
            entity: { class: Acme\UserBundle\Entity\User, property: username }
<!-- app/config/security.xml -->
<config>
    <provider name="main">
        <entity class="Acme\UserBundle\Entity\User" property="username" />
    </provider>
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    'providers' => array(
        'main' => array(
            'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'),
        ),
    ),
));

Este nuevo proveedor hace que el sistema de autenticación intente cargar los objetos de tipo User desde la base de datos utilizando el campo username de esa clase.

Nota Este ejemplo sólo muestra las opciones más básicas del proveedor entity. Para ver un ejemplo completo, consulta el artículo How to load Security Users from the Database (the Entity Provider).

Para más información sobre cómo crear tu propio proveedor de usuarios (por si quieres cargar por ejemplo tus usuarios a través de un servicio web) consulta el artículo How to create a custom User Provider.

13.5.2. Codificando la contraseña del usuario

Hasta ahora, y por simplicidad, todos los ejemplos almacenan las contraseñas de los usuarios en claro (tanto si se guardan en un archivo de configuración como si se guardan en la base de datos). Obviamente, en una aplicación real tendrás que codificar las contraseñas por motivos de seguridad. Para ello, debes asociar tu clase User con alguno de los encoders o codificadores de contraseñas definidos por Symfony. El siguiente ejemplo muestra cómo almacenar los usuarios en memoria y codificar sus contraseñas con el algoritmo sha1:

# app/config/security.yml
security:
    # ...
    providers:
        in_memory:
            memory:
                users:
                    ryan:  { password: bb87a29949f3a1ee0559f8a57357487151281386, roles: 'ROLE_USER' }
                    admin: { password: 74913f5cd5f61ec0bcfdb775414c2fb3d161b620, roles: 'ROLE_ADMIN' }

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm:  sha1
            iterations: 1
            encode_as_base64: false
<!-- app/config/security.xml -->
<config>
    <!-- ... -->
    <provider name="in_memory">
        <memory>
            <user name="ryan" password="bb87a29949f3a1ee0559f8a57357487151281386" roles="ROLE_USER" />
            <user name="admin" password="74913f5cd5f61ec0bcfdb775414c2fb3d161b620" roles="ROLE_ADMIN" />
        </memory>
    </provider>
 
    <encoder class="Symfony\Component\Security\Core\User\User" algorithm="sha1" iterations="1" encode_as_base64="false" />
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
    'providers' => array(
        'in_memory' => array(
            'memory' => array(
                'users' => array(
                    'ryan'  => array('password' => 'bb87a29949f3a1ee0559f8a57357487151281386', 'roles' => 'ROLE_USER'),
                    'admin' => array('password' => '74913f5cd5f61ec0bcfdb775414c2fb3d161b620', 'roles' => 'ROLE_ADMIN'),
                ),
            ),
        ),
    ),
    'encoders' => array(
        'Symfony\Component\Security\Core\User\User' => array(
            'algorithm'         => 'sha1',
            'iterations'        => 1,
            'encode_as_base64'  => false,
        ),
    ),
));

La opción iterations se establece a 1 y la opción encode_as_base64 a false, lo que significa que la contraseña simplemente se codifica una vez con el algoritmo sha1 y no se realiza ninguna codificación adicional. De esta forma es más fácil calcular la contraseña codificada para pegar su valor en el archivo de configuración (utiliza la instrucción hash('sha1', 'la-contraseña-en-claro')).

Si creas tus usuarios dinámicamente y los almacenas en una base de datos, puedes utilizar algoritmos hash aún más complejos y después utilizar el propio encoder de Symfony para codificar las contraseñas proporcionadas por los usuarios y así comprobar si coinciden. Imagina por ejemplo que la clase de tus usuarios es Acme\UserBundle\Entity\User (como en el ejemplo anterior). Primero, configura el encoder para ese usuario:

# app/config/security.yml
security:
    # ...

    encoders:
        Acme\UserBundle\Entity\User: sha512
<!-- app/config/security.xml -->
<config>
    <!-- ... -->
 
    <encoder class="Acme\UserBundle\Entity\User" algorithm="sha512" />
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
 
    'encoders' => array(
        'Acme\UserBundle\Entity\User' => 'sha512',
    ),
));

En este caso, estás utilizando un algoritmo más complejo llamado SHA512. Además, como solamente has especificado el algoritmo (sha512), Symfony aplica los valores por defecto al resto de opciones. Esto significa que cada contraseña se codifica 5.000 veces seguidas (cada vez se utiliza el resultado de la iteración anterior) y al final se codifica el resultado utilizando la codificación base64. Así que la contraseña se ha codificado de una manera mucho más segura que en los ejemplos anteriores (no es posible obtener la contraseña original a partir de su versión codificada).

Si utilizas un formulario de registro para los usuarios, necesitas saber el algoritmo de codificación utilizado en la contraseña, para que lo puedas aplicar a tus usuarios. Independientemente del algoritmo configures para tus usuarios, desde un controlador siempre puedes determinar el algoritmo de codificación de la contraseña de la siguiente manera:

$factory = $this->get('security.encoder_factory');
$user = new Acme\UserBundle\Entity\User();
 
$encoder = $factory->getEncoder($user);
$password = $encoder->encodePassword('ryanpass', $user->getSalt());
$user->setPassword($password);

13.5.3. Recuperando el objeto usuario

Después de la autenticación, el objeto User que representa al usuario actual se puede acceder a través del servicio security.context. Utiliza el siguiente código cuando estés dentro de un controlador:

public function indexAction()
{
    $user = $this->get('security.context')->getToken()->getUser();
}

Si tu controlador extiende del controlador base de Symfony, el código anterior se puede acortar por lo siguiente:

public function indexAction()
{
    $user = $this->getUser();
}

Nota Los usuarios anónimos técnicamente también están autenticados, lo que significa que el método isAuthenticated() del objeto que representa al usuario anónimo devuelve true. Para comprobar si el usuario está autenticado realmente, comprueba que tenga el rol IS_AUTHENTICATED_FULLY.

En las plantillas Twig puedes acceder a este objeto a través de la variable app.user, que a su vez llama al método GlobalVariables::getUser():

<p>Username: {{ app.user.username }}</p>
<p>Username: <?php echo $app->getUser()->getUsername() ?></p>

13.5.4. Utilizando varios proveedores de usuarios

Cada mecanismo de autenticación (la autenticación HTTP, el formulario de acceso, etc.) utiliza exactamente un proveedor de usuarios. Además, por defecto siempre utilizan el primer proveedor de usuarios definido. No obstante, en ocasiones puedes necesitar por ejemplo definir algunos usuarios en el archivo de configuración y el resto en una base de datos. Para conseguirlo debes definir un nuevo proveedor encadenando dos o más proveedores de usuarios con la siguiente configuración:

# app/config/security.yml
security:
    providers:
        chain_provider:
            chain:
                providers: [in_memory, user_db]
        in_memory:
            memory:
                users:
                    foo: { password: test }
        user_db:
            entity: { class: Acme\UserBundle\Entity\User, property: username }
<!-- app/config/security.xml -->
<config>
    <provider name="chain_provider">
        <chain>
            <provider>in_memory</provider>
            <provider>user_db</provider>
        </chain>
    </provider>
    <provider name="in_memory">
        <memory>
            <user name="foo" password="test" />
        </memory>
    </provider>
    <provider name="user_db">
        <entity class="Acme\UserBundle\Entity\User" property="username" />
    </provider>
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    'providers' => array(
        'chain_provider' => array(
            'chain' => array(
                'providers' => array('in_memory', 'user_db'),
            ),
        ),
        'in_memory' => array(
            'memory' => array(
               'users' => array(
                   'foo' => array('password' => 'test'),
               ),
            ),
        ),
        'user_db' => array(
            'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'),
        ),
    ),
));

Ahora todos los mecanismos de autenticación utilizan el proveedor chain_provider porque es el primero que está definido. A su vez, el chain_provider intenta cargar el usuario primero a través del proveedor in_memory y después a través de user_db.

Truco Si no vas a utilizar individualmente los proveedores in_memory y user_db, mo hace falta que los definas de forma separada. Utiliza el siguiente código para crear directamente un proveedor complejo que obtiene sus usuarios de dos fuentes distintas:

# app/config/security.yml
security:
    providers:
        main_provider:
            memory:
                users:
                    foo: { password: test }
            entity:
                class: Acme\UserBundle\Entity\User,
                property: username
<!-- app/config/security.xml -->
<config>
    <provider name=="main_provider">
        <memory>
            <user name="foo" password="test" />
        </memory>
        <entity class="Acme\UserBundle\Entity\User" property="username" />
    </provider>
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    'providers' => array(
        'main_provider' => array(
            'memory' => array(
                'users' => array(
                    'foo' => array('password' => 'test'),
                ),
            ),
            'entity' => array('class' => 'Acme\UserBundle\Entity\User', 'property' => 'username'),
        ),
    ),
));

También puedes configurar cada firewall para que utilice un proveedor de usuarios específico. Esto es importante porque, como ya sabes, a menos que especifiques explícitamente un proveedor, siempre se utiliza el primer proveedor definido:

# app/config/security.yml
security:
    firewalls:
        secured_area:
            # ...
            provider: user_db
            http_basic:
                realm: "Secured Demo Area"
                provider: in_memory
            form_login: ~
<!-- app/config/security.xml -->
<config>
    <firewall name="secured_area" pattern="^/" provider="user_db">
        <!-- ... -->
        <http-basic realm="Secured Demo Area" provider="in_memory" />
        <form-login />
    </firewall>
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    'firewalls' => array(
        'secured_area' => array(
            ...,
            'provider' => 'user_db',
            'http_basic' => array(
                ...,
                'provider' => 'in_memory',
            ),
            'form_login' => array(),
        ),
    ),
));

En este ejemplo, si un usuario intenta acceder a través de la autenticación HTTP, el sistema de autenticación utiliza el proveedor in_memory. Pero si el usuario intenta acceder a través del formulario de acceso, se utiliza el proveedor user_db (ya que es el valor por defecto de todo el firewall).

Para más información acerca de los proveedores de usuario y la configuración del firewall, consulta el artículo Security Configuration Reference.