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.

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

# app/config/security.yml
security:
    # ...
    providers:
        in_memory:
            memory:
                users:
                    ryan:
                        password: $2a$12$w/aHvnC/XNeDVrrl65b3dept8QcKqpADxUlbraVXXsC03Jam5hvoO
                        roles: 'ROLE_USER'
                    admin:
                        password: $2a$12$HmOsqRDJK0HuMDQ5Fb2.AOLMQHyNHGD0seyjU3lEVusjT72QQEIpW
                        roles: 'ROLE_ADMIN'

    encoders:
        Symfony\Component\Security\Core\User\User:
            algorithm: bcrypt
            cost:      12
<!-- app/config/security.xml -->
<config>
    <!-- ... -->
    <provider name="in_memory">
        <memory>
            <user name="ryan"
                password="$2a$12$w/aHvnC/XNeDVrrl65b3dept8QcKqpADxUlbraVXXsC03Jam5hvoO"
                roles="ROLE_USER" />
            <user name="admin"
                password="$2a$12$HmOsqRDJK0HuMDQ5Fb2.AOLMQHyNHGD0seyjU3lEVusjT72QQEIpW"
                roles="ROLE_ADMIN" />
        </memory>
    </provider>
 
    <encoder class="Symfony\Component\Security\Core\User\User"
        algorithm="bcrypt"
        cost="12"
    />
</config>
// app/config/security.php
$container->loadFromExtension('security', array(
    // ...
    'providers' => array(
        'in_memory' => array(
            'memory' => array(
                'users' => array(
                    'ryan'  => array(
                        'password' => '$2a$12$w/aHvnC/XNeDVrrl65b3dept8QcKqpADxUlbraVXXsC03Jam5hvoO',
                        'roles'    => 'ROLE_USER'
                    ),
                    'admin' => array(
                        'password' => '$2a$12$HmOsqRDJK0HuMDQ5Fb2.AOLMQHyNHGD0seyjU3lEVusjT72QQEIpW',
                        'roles'    => 'ROLE_ADMIN'
                    ),
                ),
            ),
        ),
    ),
    'encoders' => array(
        'Symfony\Component\Security\Core\User\User' => array(
            'algorithm' => 'bcrypt',
            'cost'      => 12,
        ),
    ),
));

El codificador bcrypt se añadió en la versión 2.2 de Symfony. Ahora ya puedes codificar la contraseña mediante la función password_hash('ryanpass', PASSWORD_BCRYPT, array('cost' => 12)); de PHP.

Advertencia Si tu versión de PHP es 5.4 o anterior, para utilizar el codificador bcrypt tendrás que instalar la librería ircmaxell/password-compat mediante Composer. Para ello, añade la siguiente dependencia en tu archivo composer.json:

{
    "require": {
        ...
        "ircmaxell/password-compat": "~1.0.3"
    }
}

Los algoritmos de codificación que puedes utilizar en el componente de seguridad de Symfony dependen de tu versión de PHP. Para obtener la lista completa de algoritmos disponibles, ejecuta la función hash_algos() de PHP.

A partir de la versión 2.2 de Symfony, también puedes utilizar PBKDF2 para codificar las contraseñas.

13.5.2.1. Determinando las contraseñas codificadas

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

Para que el código anterior funcione correctamente, asegúrate de haber asociado un encoder a la clase de tus usuarios (por ejemplo Acme\UserBundle\Entity\User) en la clave encoders del archivo app/config/security.yml.

Advertencia Por motivos de seguridad, Symfony2 limita la longitud máxima de una contraseña en claro a 4096 caracteres. Asegúrate de añadir las restricciones de validación adecuadas para asegurar que la longitud de la contraseña no excede ese límite.

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.

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.