Seguridad

La seguridad es un proceso de dos etapas, cuyo objetivo es evitar que un usuario acceda a un recurso al cual no debería tener acceso.

En el primer paso del proceso, el sistema de seguridad identifica quién es el usuario obligándolo a presentar algún tipo de identificación. Esto se llama autenticación, y significa que el sistema está tratando de determinar quién eres.

Una vez que el sistema sabe quien eres, el siguiente paso es determinar si deberías tener acceso a un determinado recurso. Esta parte del proceso se llama autorización, y significa que el sistema está comprobando si tienes suficientes privilegios para realizar una determinada acción.

../_images/security_authentication_authorization_es.png

Puesto que la mejor manera de aprender es viendo un ejemplo, empieza asegurando tu aplicación con autenticación HTTP básica.

Nota

El componente Security de Symfony está disponible como una biblioteca PHP independiente para usarla en cualquier proyecto PHP.

Ejemplo básico: Autenticación HTTP

Puedes ajustar el componente de seguridad a través de la configuración de tu aplicación. De hecho, la mayoría de las opciones de seguridad estándar son sólo cuestión de usar los ajustes correctos. La siguiente configuración le dice a Symfony que proteja cualquier URL coincidente con /admin/* y pida al usuario sus credenciales mediante autenticación HTTP básica (es decir, el cuadro de dialogo a la vieja escuela: nombre de usuario/contraseña):

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                pattern:    ^/
                anonymous: ~
                http_basic:
                    realm: "Secured Demo Area"
    
        access_control:
            - { path: ^/admin, roles: ROLE_ADMIN }
    
        providers:
            in_memory:
                memory:
                    users:
                        ryan:  { password: ryanpass, roles: 'ROLE_USER' }
                        admin: { password: kitten, roles: 'ROLE_ADMIN' }
    
        encoders:
            Symfony\Component\Security\Core\User\User: plaintext
    
  • XML
    <?xml version="1.0" encoding="UTF-8"?>
    
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <!-- app/config/security.xml -->
    
        <config>
            <firewall name="secured_area" pattern="^/">
                <anonymous />
                <http-basic realm="Secured Demo Area" />
            </firewall>
    
            <access-control>
                <rule path="^/admin" role="ROLE_ADMIN" />
            </access-control>
    
            <provider name="in_memory">
                <memory>
                    <user name="ryan" password="ryanpass" roles="ROLE_USER" />
                    <user name="admin" password="kitten" roles="ROLE_ADMIN" />
                </memory>
            </provider>
    
            <encoder class="Symfony\Component\Security\Core\User\User" algorithm="plaintext" />
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                'pattern'    => '^/',
                'anonymous'  => array(),
                'http_basic' => array(
                    'realm'  => 'Secured Demo Area',
                ),
            ),
        ),
        'access_control' => array(
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
        'providers' => array(
            'in_memory' => array(
                'memory' => array(
                    'users' => array(
                        'ryan' => array('password' => 'ryanpass', 'roles' => 'ROLE_USER'),
                        'admin' => array('password' => 'kitten', 'roles' => 'ROLE_ADMIN'),
                    ),
                ),
            ),
        ),
        'encoders' => array(
            'Symfony\Component\Security\Core\User\User' => 'plaintext',
        ),
    ));
    

Truco

Una distribución estándar de Symfony separa la configuración de seguridad en un archivo independiente (por ejemplo, app/config/security.yml). Si no tienes un archivo de seguridad autónomo, puedes poner la configuración directamente en el archivo de configuración principal (por ejemplo, app/config/config.yml).

El resultado final de esta configuración es un sistema de seguridad totalmente operativo que tiene el siguiente aspecto:

  • Hay dos usuarios en el sistema (ryan y admin);
  • Los usuarios se autentican a través de la autenticación HTTP básica del sistema;
  • Cualquier URL que coincida con /admin/* está protegida, y sólo el usuario admin puede acceder a ellas;
  • Todas las URL que no coincidan con /admin/* son accesibles para todos los usuarios (y nunca se pide al usuario que se registre).

Veamos brevemente cómo funciona la seguridad y cómo entra en juego cada parte de la configuración.

Cómo funciona la seguridad: autenticación y autorización

El sistema de seguridad de Symfony trabaja identificando a un usuario (es decir, la autenticación) y comprobando si ese usuario debe tener acceso a una URL o recurso específico.

Cortafuegos (autenticación)

Cuando un usuario hace una petición a una URL que está protegida por un cortafuegos, se activa el sistema de seguridad. El trabajo del cortafuegos es determinar si el usuario necesita estar autenticado, y si lo hace, enviar una respuesta al usuario para iniciar el proceso de autenticación.

Un cortafuegos se activa cuando la URL de una petición entrante concuerda con el patrón de la expresión regular configurada en el valor config del cortafuegos. En este ejemplo el patrón (^/) concordará con cada petición entrante. El hecho de que el cortafuegos esté activado no significa, sin embargo, que el nombre de usuario de autenticación HTTP y el cuadro de diálogo de la contraseña se muestre en cada URL. Por ejemplo, cualquier usuario puede acceder a /foo sin que se le pida se autentique.

../_images/security_anonymous_user_access_es.png

Esto funciona en primer lugar porque el cortafuegos permite usuarios anónimos a través del parámetro de configuración anonymous. En otras palabras, el cortafuegos no requiere que el usuario se autentique plenamente de inmediato. Y puesto que no hay rol especial necesario para acceder a /foo (bajo la sección access_control), la petición se puede llevar a cabo sin solicitar al usuario se autentique.

Si eliminas la clave anonymous, el cortafuegos siempre hará que un usuario se autentique inmediatamente.

Control de acceso (autorización)

Si un usuario solicita /admin/foo, sin embargo, el proceso se comporta de manera diferente. Esto se debe a la sección de configuración access_control la cual dice que cualquier URL coincidente con el patrón de la expresión regular ^/admin (es decir, /admin o cualquier cosa coincidente con /admin/*) requiere el rol ROLE_ADMIN. Los roles son la base para la mayor parte de la autorización: el usuario puede acceder a /admin/foo sólo si cuenta con el rol ROLE_ADMIN.

../_images/security_anonymous_user_denied_authorization_es.png

Como antes, cuando el usuario hace la petición originalmente, el cortafuegos no solicita ningún tipo de identificación. Sin embargo, tan pronto como la capa de control de acceso niega el acceso a los usuarios (ya que el usuario anónimo no tiene el rol ROLE_ADMIN), el servidor de seguridad entra en acción e inicia el proceso de autenticación). El proceso de autenticación depende del mecanismo de autenticación que utilices. Por ejemplo, si estás utilizando el método de autenticación con formulario de acceso, el usuario será redirigido a la página de inicio de sesión. Si estás utilizando autenticación HTTP, se enviará al usuario una respuesta HTTP 401 para que el usuario vea el cuadro de diálogo de nombre de usuario y contraseña.

Ahora el usuario de nuevo tiene la posibilidad de presentar sus credenciales a la aplicación. Si las credenciales son válidas, se puede intentar de nuevo la petición original.

../_images/security_ryan_no_role_admin_access_es.png

En este ejemplo, el usuario ryan se autentica correctamente con el cortafuegos. Pero como ryan no cuenta con el rol ROLE_ADMIN, se le sigue negando el acceso a /admin/foo. En última instancia, esto significa que el usuario debe ver algún tipo de mensaje indicándole que se le ha denegado el acceso.

Truco

Cuando Symfony niega el acceso al usuario, él verá una pantalla de error y recibe un código de estado HTTP 403 (Prohibido). Puedes personalizar la pantalla de error, acceso denegado, siguiendo las instrucciones de las Páginas de error en el artículo para personalizar la página de error 403 del recetario.

Por último, si el usuario admin solicita /admin/foo, se lleva a cabo un proceso similar, excepto que ahora, después de haberse autenticado, la capa de control de acceso le permitirá pasar a través de la petición:

../_images/security_admin_role_access_es.png

El flujo de la petición cuando un usuario solicita un recurso protegido es sencillo, pero increíblemente flexible. Como verás más adelante, la autenticación se puede realizar de varias maneras, incluyendo a través de un formulario de acceso, certificados X.509 o la autenticación del usuario a través de Twitter. Independientemente del método de autenticación, el flujo de la petición siempre es el mismo:

  1. Un usuario accede a un recurso protegido;
  2. La aplicación redirige al usuario al formulario de acceso;
  3. El usuario presenta sus credenciales (por ejemplo nombre de usuario/contraseña);
  4. El cortafuegos autentica al usuario;
  5. El nuevo usuario autenticado intenta de nuevo la petición original.

Nota

El proceso exacto realmente depende un poco en el mecanismo de autenticación utilizado. Por ejemplo, cuando utilizas el formulario de acceso, el usuario presenta sus credenciales a una URL que procesa el formulario (por ejemplo /login_check) y luego es redirigido a la dirección solicitada originalmente (por ejemplo /admin/foo). Pero con la autenticación HTTP, el usuario envía sus credenciales directamente a la URL original (por ejemplo /admin/foo) y luego la página se devuelve al usuario en la misma petición (es decir, sin redirección).

Este tipo de idiosincrasia no debería causar ningún problema, pero es bueno tenerla en cuenta.

Truco

También aprenderás más adelante cómo puedes proteger cualquier cosa en Symfony2, incluidos controladores específicos, objetos, e incluso métodos PHP.

Usando un formulario de acceso tradicional

Truco

En esta sección, aprenderás cómo crear un formulario de acceso básico que continúa usando los usuarios definidos en el código del archivo security.yml.

Para cargar usuarios desde la base de datos, por favor consulta Cómo cargar usuarios desde la base de datos con seguridad (el Proveedor de entidad). Al leer este artículo y esta sección, puedes crear un sistema de formularios de acceso completo que carga usuarios desde la base de datos.

Hasta ahora, hemos visto cómo cubrir tu aplicación bajo un cortafuegos y proteger el acceso a determinadas zonas con roles. Al usar la autenticación HTTP, puedes aprovechar sin esfuerzo, el cuadro de diálogo nativo nombre de usuario/contraseña que ofrecen todos los navegadores. Sin embargo, fuera de la caja, Symfony es compatible con múltiples mecanismos de autenticación. Para información detallada sobre todos ellos, consulta la Referencia para afinar el sistema de seguridad.

En esta sección, vamos a mejorar este proceso permitiendo la autenticación del usuario a través de un formulario de acceso HTML tradicional.

En primer lugar, activa el formulario de acceso en el cortafuegos:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                pattern:    ^/
                anonymous: ~
                form_login:
                    login_path:  login
                    check_path:  login_check
    
  • XML
    <?xml version="1.0" encoding="UTF-8"?>
    
    <srv:container xmlns="http://symfony.com/schema/dic/security"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:srv="http://symfony.com/schema/dic/services"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
    
        <!-- app/config/security.xml -->
    
        <config>
            <firewall name="secured_area" pattern="^/">
                <anonymous />
                <form-login login_path="login" check_path="login_check" />
            </firewall>
        </config>
    </srv:container>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                'pattern'    => '^/',
                'anonymous'  => array(),
                'form_login' => array(
                    'login_path' => 'login',
                    'check_path' => 'login_check',
                ),
            ),
        ),
    ));
    

Truco

Si no necesitas personalizar tus valores login_path o check_path (los valores utilizados aquí son los valores predeterminados), puedes acortar tu configuración:

  • YAML
    form_login: ~
    
  • XML
    <form-login />
    
  • PHP
    'form_login' => array(),
    

Ahora, cuando el sistema de seguridad inicia el proceso de autenticación, redirige al usuario al formulario de acceso (predeterminado a /login). La implementación visual de este formulario de acceso es tu trabajo. Primero, crea las dos rutas que utilizarás en la configuración de seguridad: La ruta login mostrará el formulario de inicio de sesión (es decir /login) y la ruta login_check procesará el formulario enviado (es decir /login_check):

  • YAML
    # app/config/routing.yml
    login:
        pattern:   /login
        defaults:  { _controller: AcmeSecurityBundle:Security:login }
    login_check:
        pattern:   /login_check
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="login" pattern="/login">
            <default key="_controller">AcmeSecurityBundle:Security:login</default>
        </route>
        <route id="login_check" pattern="/login_check" />
    
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('login', new Route('/login', array(
        '_controller' => 'AcmeDemoBundle:Security:login',
    )));
    $collection->add('login_check', new Route('/login_check', array()));
    
    return $collection;
    

Nota

No necesitas implementar un controlador para la URL /login_check ya que el cortafuegos automáticamente captura y procesa cualquier formulario enviado a esta URL.

Nuevo en la versión 2.1: A partir de Symfony 2.1, debes tener configuradas las rutas para tus claves login_path, check_path y logout. Estas claves pueden ser nombres de ruta (tal como muestra este ejemplo) o las URL que tienen rutas configuradas para ello.

Ten en cuenta que el nombre de la ruta login coincide con el valor login_path configurado, a donde el sistema de seguridad redirigirá a los usuarios que necesiten ingresar.

A continuación, crea el controlador que mostrará el formulario de acceso:

// src/Acme/SecurityBundle/Controller/SecurityController.php;
namespace Acme\SecurityBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;

class SecurityController extends Controller
{
    public function loginAction()
    {
        $request = $this->getRequest();
        $session = $request->getSession();

        // obtiene el error de inicio de sesión si lo hay
        if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
            $error = $request->attributes->get(
                SecurityContext::AUTHENTICATION_ERROR
            );
        } else {
            $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
            $session->remove(SecurityContext::AUTHENTICATION_ERROR);
        }

        return $this->render(
            'AcmeSecurityBundle:Security:login.html.twig',
            array(
                // último nombre de usuario ingresado
                'last_username' => $session->get(SecurityContext::LAST_USERNAME),
                'error'         => $error,
            )
        );
    }
}

No dejes que este controlador te confunda. Como veremos en un momento, cuando el usuario envía el formulario, el sistema de seguridad automáticamente se encarga de procesar la recepción del formulario por ti. Si el usuario ha presentado un nombre de usuario o contraseña no válidos, este controlador lee el error del formulario enviado desde el sistema de seguridad de modo que se pueda mostrar al usuario.

En otras palabras, tu trabajo es mostrar el formulario al usuario y los errores de ingreso que puedan haber ocurrido, pero, el propio sistema de seguridad se encarga de verificar el nombre de usuario y contraseña y la autenticación del usuario.

Por último, crea la plantilla correspondiente:

  • Twig
    {# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
    {% if error %}
        <div>{{ error.message }}</div>
    {% endif %}
    
    <form action="{{ path('login_check') }}" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="{{ last_username }}" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        {#
            Si deseas controlar la URL a la que rediriges al
            usuario en caso de éxito (más detalles abajo)
            <input type="hidden" name="_target_path" value="/account" />
        #}
    
        <button type="submit">login</button>
    </form>
    
  • PHP
    <!-- src/Acme/SecurityBundle/Resources/views/Security/login.html.php -->
    <?php if ($error): ?>
        <div><?php echo $error->getMessage() ?></div>
    <?php endif; ?>
    
    <form action="<?php echo $view['router']->generate('login_check') ?>" method="post">
        <label for="username">Username:</label>
        <input type="text" id="username" name="_username" value="<?php echo $last_username ?>" />
    
        <label for="password">Password:</label>
        <input type="password" id="password" name="_password" />
    
        <!--
            Si deseas controlar la URL a la que rediriges al usuario en caso de éxito (más detalles abajo)
            <input type="hidden" name="_target_path" value="/account" />
        -->
    
        <button type="submit">login</button>
    </form>
    

Truco

La variable error pasada a la plantilla es una instancia de Symfony\Component\Security\Core\Exception\AuthenticationException. Esta puede contener más información —o incluso información confidencial— sobre el fallo de autenticación, ¡por lo tanto utilízala prudentemente!

El formulario tiene muy pocos requisitos. En primer lugar, presentando el formulario a /login_check (a ​​través de la ruta login_check), el sistema de seguridad debe interceptar el envío del formulario y procesarlo automáticamente. En segundo lugar, el sistema de seguridad espera que los campos presentados se llamen _username y _password (estos nombres de campo se pueden configurar).

¡Y eso es todo! Cuando envías el formulario, el sistema de seguridad automáticamente comprobará las credenciales del usuario y, o bien autenticará al usuario o lo enviará al formulario de acceso donde se puede mostrar el error.

Revisemos todo el proceso:

  1. El usuario intenta acceder a un recurso que está protegido;
  2. El cortafuegos inicia el proceso de autenticación redirigiendo al usuario al formulario de acceso (/login);
  3. La página /login reproduce el formulario de acceso a través de la ruta y el controlador creado en este ejemplo;
  4. El usuario envía el formulario de acceso a /login_check;
  5. El sistema de seguridad intercepta la petición, comprueba las credenciales presentadas por el usuario, autentica al usuario si todo está correcto, y si no, envía al usuario de nuevo al formulario de acceso.

Por omisión, si las credenciales presentadas son correctas, el usuario será redirigido a la página solicitada originalmente (por ejemplo /admin/foo). Si originalmente el usuario fue directo a la página de inicio de sesión, será redirigido a la página principal. Esto puede ser altamente personalizado, lo cual te permite, por ejemplo, redirigir al usuario a una URL específica.

Para más detalles sobre esto y cómo personalizar el proceso de entrada en general, consulta Cómo personalizar el formulario de acceso.

Autorizando

El primer paso en la seguridad siempre es la autenticación: el proceso de verificar quién es el usuario. Con Symfony, la autenticación se puede hacer de cualquier manera —a través de un formulario de acceso, autenticación básica HTTP, e incluso a través de Facebook.

Una vez que el usuario se ha autenticado, comienza la autorización. La autorización proporciona una forma estándar y potente para decidir si un usuario puede acceder a algún recurso (una URL, un modelo de objetos, una llamada a un método, ...). Esto funciona asignando roles específicos a cada usuario y, a continuación, requiriendo diferentes roles para diferentes recursos.

El proceso de autorización tiene dos lados diferentes:

  1. El usuario tiene un conjunto de roles específico;
  2. Un recurso requiere un rol específico a fin de tener acceso.

En esta sección, nos centraremos en cómo proteger diferentes recursos (por ejemplo, URL, llamadas a métodos, etc.) con diferentes roles. Más tarde, aprenderás más sobre cómo crear y asignar roles a los usuarios.

Protegiendo patrones de URL específicas

La forma más básica para proteger parte de tu aplicación es asegurar un patrón de URL completo. Ya lo viste en el primer ejemplo de este capítulo, cómo algo que coincide con el patrón de la expresión regular ^/admin requiere el rol ROLE_ADMIN.

Prudencia

Entender exactamente cómo trabaja access_control es muy importante para garantizar que tu aplicación está protegida correctamente. Ve más adelante Entendiendo cómo trabaja access_control para información detallada.

Puedes definir tantos patrones URL como necesites —cada uno es una expresión regular—.

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/admin/users, roles: ROLE_SUPER_ADMIN }
            - { path: ^/admin, roles: ROLE_ADMIN }
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <!-- ... -->
        <rule path="^/admin/users" role="ROLE_SUPER_ADMIN" />
        <rule path="^/admin" role="ROLE_ADMIN" />
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        // ...
        'access_control' => array(
            array('path' => '^/admin/users', 'role' => 'ROLE_SUPER_ADMIN'),
            array('path' => '^/admin', 'role' => 'ROLE_ADMIN'),
        ),
    ));
    

Truco

Al prefijar la ruta con ^ te aseguras que sólo coinciden las URL que comienzan con ese patrón. Por ejemplo, una ruta de simplemente /admin (sin el ^) correctamente coincidirá con /admin/foo pero también coincide con la URL /foo/admin.

Entendiendo cómo trabaja access_control

Por cada petición entrante, Symfony2 comprueba cada opción access_control para encontrar una que concuerde con la petición actual. Tan pronto como encuentra una entrada access_control coincidente, se detiene —únicamente si el primer access_control concordante se usa para forzar el acceso—.

Cada access_control tiene varias opciones que configuran dos diferentes cosas: (a) la petición entrante emparejada debe tener esta entrada de control de acceso y (b) una vez emparejada, debe tener algún tipo de restricción de acceso aplicable:

(a) Emparejando Opciones

Symfony2 crea una instancia de Symfony\Component\HttpFoundation\RequestMatcher para cada entrada access_control, la cual determina si o no se debería usar un determinado control de acceso en esa petición. Las siguientes opciones de access_control se utilizan para emparejar:

  • path
  • ip
  • host
  • methods

Toma las siguientes entradas de access_control como ejemplo:

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/admin, roles: ROLE_USER_IP, ip: 127.0.0.1 }
            - { path: ^/admin, roles: ROLE_USER_HOST, host: symfony.com }
            - { path: ^/admin, roles: ROLE_USER_METHOD, methods: [POST, PUT] }
            - { path: ^/admin, roles: ROLE_USER }
    
  • XML
    <access-control>
        <rule path="^/admin" role="ROLE_USER_IP" ip="127.0.0.1" />
        <rule path="^/admin" role="ROLE_USER_HOST" host="symfony.com" />
        <rule path="^/admin" role="ROLE_USER_METHOD" method="POST, PUT" />
        <rule path="^/admin" role="ROLE_USER" />
    </access-control>
    
  • PHP
    'access_control' => array(
        array('path' => '^/admin', 'role' => 'ROLE_USER_IP', 'ip' => '127.0.0.1'),
        array('path' => '^/admin', 'role' => 'ROLE_USER_HOST', 'host' => 'symfony.com'),
        array('path' => '^/admin', 'role' => 'ROLE_USER_METHOD', 'method' => 'POST, PUT'),
        array('path' => '^/admin', 'role' => 'ROLE_USER'),
    ),
    

Para cada petición entrante, Symfony debe decidir cuál access_control utilizar basándose en la URI, la dirección IP del cliente, el nombre del servidor entrante, y el método de la petición. Recuerda, se usa la primera regla que coincida, y si para una entrada no se especifican ip, host o method, ese access_control emparejará con cualquier ip, host o method:

URI IP HOST METHOD access_control ¿Porqué?
/admin/user 127.0.0.1 example.com GET regla #1 (ROLE_USER_IP) La URI empareja con path y la IP con ip.
/admin/user 127.0.0.1 symfony.com GET regla #1 (ROLE_USER_IP) path e ip todavía concuerdan. Esta además debería emparejar con la entrada ROLE_USER_HOST, pero sólo si se usa el primer access_control coincidente.
/admin/user 168.0.0.1 symfony.com GET regla #2 (ROLE_USER_HOST) La ip no concuerda con la primera regla, por lo tanto se usa la segunda regla (la cual concuerda).
/admin/user 168.0.0.1 symfony.com POST regla #2 (ROLE_USER_HOST) La segunda regla todavía concuerda. Esta además debería emparejar con la tercera regla (ROLE_USER_METHOD), pero solo si se usa la primer access_control coincidente.
/admin/user 168.0.0.1 example.com POST regla #3 (ROLE_USER_METHOD) La ip y host no concuerdan con las primeras dos entradas, pero la tercera —ROLE_USER_METHOD— concuerda y se usa.
/admin/user 168.0.0.1 example.com GET regla #4 (ROLE_USER) La ip, host y method evitan que las primeras tres entradas concuerden. Pero debido a que la URI concuerda con el patrón path de la entrada ROLE_USER, esta se usa.
/foo 127.0.0.1 symfony.com POST no hay entradas concordantes Esta no concuerda con ninguna regla access_control, debido a que la URI no concuerda con los valores de path.

(b) Forzando el acceso

Una vez que Symfony2 ha decidido cuál entrada access_control concuerda (si la hay), entonces aplica las restricciones de acceso basándose en las opciones roles y requires_channel:

  • role Si el usuario no tiene determinado rol o roles, entonces el acceso es denegado (internamente, se lanza una Symfony\Component\Security\Core\Exception\AccessDeniedException);
  • requires_channel Si el canal de la petición entrante (p. ej. http) no concuerda con este valor (p. ej. https), el usuario será redirigido (p. ej. redirigido de http a https, o viceversa).

Truco

Si el acceso es denegado, el sistema intentará autenticar al usuario si aún no lo está (p. ej. redirigiendo al usuario a la página de inicio de sesión). Si el usuario ya inició sesión, se mostrará la página del error 403 «acceso denegado». Consulta Cómo personalizar páginas de error para más información.

Protegiendo por IP

En algunas situaciones puede surgir la necesidad de restringir el acceso a una determinada ruta basándose en la IP. Esto es importante particularmente en el caso de la Inclusión del borde lateral (ESI), por ejemplo. Cuándo ESI está habilitada, es recomendable proteger el acceso a direcciones URL ESI. De hecho, algunas ESI pueden contener algo de contenido privado tal como la información del usuario actual. Para impedir cualquier acceso directo a estos recursos desde un navegador web (deduciendo el patrón ESI de la URL), la ruta ESI se debe asegurar para que únicamente sea visible desde la caché de un delegado inverso de confianza.

Aquí tienes un ejemplo de cómo podrías proteger todas las rutas ESI que empiezan con un determinado prefijo, /esi, para que no se accedan desde el exterior:

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/esi, roles: IS_AUTHENTICATED_ANONYMOUSLY, ip: 127.0.0.1 }
            - { path: ^/esi, roles: ROLE_NO_ACCESS }
    
  • XML
    <access-control>
        <rule path="^/esi" role="IS_AUTHENTICATED_ANONYMOUSLY" ip="127.0.0.1" />
        <rule path="^/esi" role="ROLE_NO_ACCESS" />
    </access-control>
    
  • PHP
    'access_control' => array(
        array('path' => '^/esi', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'ip' => '127.0.0.1'),
        array('path' => '^/esi', 'role' => 'ROLE_NO_ACCESS'),
    ),
    

Así es como trabaja cuándo la ruta es /esi/algo proveniente de la IP 10.0.0.1:

  • La primera regla del control de acceso es ignorada debido a que path concuerda pero la ip no;
  • La segunda regla de control de acceso se activa (la única restricción sigue siendo path y concuerda): Puesto que el usuario no puede tener el rol ROLE_NO_ACCESS cuando no está definido, el acceso es denegado (el rol ROLE_NO_ACCESS puede ser cualquier cosa que no empareje con un rol existente, solo sirve como un truco para negar el acceso siempre).

Ahora, si la misma petición proviene de 127.0.0.1:

  • Ahora, se activa la primera regla del control de acceso porque ambas path e ip concuerdan: El acceso es permitido debido a que el usuario siempre tiene el rol IS_AUTHENTICATED_ANONYMOUSLY.
  • La segunda regla de acceso no se examina debido a que la primera regla concordó.

Protegiendo por canal

También puedes requerir que un usuario acceda a una URL vía SSL; Sólo utiliza el argumento requires_channel en cualquier entrada del access_control:

  • YAML
    # app/config/security.yml
    security:
        # ...
        access_control:
            - { path: ^/cart/checkout, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }
    
  • XML
    <access-control>
        <rule path="^/cart/checkout" role="IS_AUTHENTICATED_ANONYMOUSLY" requires_channel="https" />
    </access-control>
    
  • PHP
    'access_control' => array(
        array('path' => '^/cart/checkout', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY', 'requires_channel' => 'https'),
    ),
    

Protegiendo un controlador

Proteger tu aplicación basándote en los patrones URL es fácil, pero, en algunos casos, puede no estar suficientemente bien ajustado. Cuando sea necesario, fácilmente puedes forzar la autorización desde un controlador:

// ...
use Symfony\Component\Security\Core\Exception\AccessDeniedException;

public function helloAction($name)
{
    if (false === $this->get('security.context')->isGranted('ROLE_ADMIN')) {
        throw new AccessDeniedException();
    }

    // ...
}

También puedes optar por instalar y utilizar el opcional JMSSecurityExtraBundle, con el cual puedes asegurar tu controlador usando anotaciones:

// ...
use JMS\SecurityExtraBundle\Annotation\Secure;

/**
 * @Secure(roles="ROLE_ADMIN")
 */
public function helloAction($name)
{
    // ...
}

Para más información, consulta la documentación de JMSSecurityExtraBundle. Si estás usando la distribución estándar de Symfony, este paquete está disponible de forma predeterminada. Si no es así, lo puedes descargar e instalar.

Protegiendo otros servicios

De hecho, en Symfony puedes proteger cualquier cosa utilizando una estrategia similar a la observada en la sección anterior. Por ejemplo, supongamos que tienes un servicio (es decir, una clase PHP), cuyo trabajo consiste en enviar mensajes de correo electrónico de un usuario a otro. Puedes restringir el uso de esta clase —no importa dónde se esté utilizando— a los usuarios que tienen un rol específico.

Para más información sobre cómo utilizar el componente de seguridad para proteger diferentes servicios y métodos en tu aplicación, consulta Cómo proteger cualquier servicio o método de tu aplicación.

Listas de control de acceso (ACL): Protegiendo objetos individuales de base de datos

Imagina que estás diseñando un sistema de blog donde los usuarios pueden comentar tus entradas. Ahora, deseas que un usuario pueda editar sus propios comentarios, pero no los de otros usuarios. Además, como usuario admin, quieres tener la posibilidad de editar todos los comentarios.

El componente de seguridad viene con un sistema opcional de lista de control de acceso (ACL) que puedes utilizar cuando sea necesario para controlar el acceso a instancias individuales de un objeto en el sistema. Sin ACL, puedes proteger tu sistema para que sólo determinados usuarios puedan editar los comentarios del blog en general. Pero con ACL, puedes restringir o permitir el acceso en base a comentario por comentario.

Para más información, consulta el artículo del recetario: Cómo usar las listas para el control de acceso (ACL).

Usuarios

En las secciones anteriores, aprendiste cómo puedes proteger diferentes recursos que requieren un conjunto de roles para un recurso. En esta sección vamos a explorar el otro lado de la autorización: los usuarios.

¿De dónde provienen los usuarios? (Proveedores de 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 concordar esas credenciales contra una piscina de usuarios. Entonces, ¿de dónde viene esta lista de usuarios?

En Symfony2, los usuarios pueden venir de cualquier parte —un archivo de configuración, una tabla de base de datos, un servicio web, o cualquier otra cosa que se te ocurra. Todo lo que proporcione uno o más usuarios al sistema de autenticación se conoce como «proveedor de usuario». Symfony2 de serie viene con los dos proveedores de usuario 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.

Especificando usuarios en un archivo de configuración

La forma más fácil para especificar usuarios es directamente en un archivo de configuración. De hecho, ya lo has visto en algunos ejemplos de este capítulo.

  • YAML
    # app/config/security.yml
    security:
        # ...
        providers:
            default_provider:
                memory:
                    users:
                        ryan:  { password: ryanpass, roles: 'ROLE_USER' }
                        admin: { password: kitten, roles: 'ROLE_ADMIN' }
    
  • XML
    <!-- 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>
    
  • PHP
    // 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 de usuario se denomina proveedor de usuario «en memoria», ya que los usuarios no se almacenan en alguna parte de una base de datos. El objeto usuario en realidad lo proporciona Symfony (Symfony\Component\Security\Core\User\User).

Truco

Cualquier proveedor de usuario puede cargar usuarios directamente desde la configuración especificando el parámetro de configuración users y la lista de usuarios debajo de él.

Prudencia

Si tu nombre de usuario es completamente numérico (por ejemplo, 77) o contiene un guión (por ejemplo, user-name), debes utilizar la sintaxis alterna al especificar usuarios en 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 sistemas más complejos, querrás cargar usuarios desde la base de datos.

Cargando usuarios de la base de datos

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

Truco

Hay disponible un paquete de código abierto de alta calidad, el cual te permite almacenar tus usuarios a través del ORM u ODM de Doctrine. Lee más acerca del FOSUserBundle en GitHub.

Con este enfoque, primero crea tu propia clase User, la cual se almacenará 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;

    // ...
}

En cuanto al sistema de seguridad se refiere, el único requisito para tu clase Usuario personalizada es que implemente la interfaz Symfony\Component\Security\Core\User\UserInterface. Esto significa que el concepto de un «usuario» puede ser cualquier cosa, siempre y cuando implemente esta interfaz.

Nuevo en la versión 2.1: En Symfony 2.1, se removió el método equals de la UserInterface. Si necesitas sustituir la implementación predeterminada de la lógica de comparación, implementa la nueva interfaz Symfony\Component\Security\Core\User\EquatableInterface.

Nota

El objeto User se debe serializar y guardar en la sesión entre peticiones, por lo tanto se recomienda que implementes la interfaz Serializable en tu objeto que representa al usuario. Esto es especialmente importante si tu clase User tiene una clase padre con propiedades privadas.

A continuación, configura una entidad proveedora de usuario, y apúntala a tu clase User:

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

Con la introducción de este nuevo proveedor, el sistema de autenticación intenta cargar un objeto User de la base de datos utilizando el campo username de esa clase.

Nota

Este ejemplo sólo intenta mostrar la idea básica detrás del proveedor entity. Para ver un ejemplo completo funcionando, consulta Cómo cargar usuarios desde la base de datos con seguridad (el Proveedor de entidad).

Para más información sobre cómo crear tu propio proveedor personalizado (por ejemplo, si necesitas cargar usuarios a través de un servicio Web), consulta Cómo crear un proveedor de usuario personalizado.

Codificando la contraseña del usuario

Hasta ahora, por simplicidad, todos los ejemplos tienen las contraseñas de los usuarios almacenadas en texto plano (si los usuarios se almacenan en un archivo de configuración o en alguna base de datos). Por supuesto, en una aplicación real, por razones de seguridad, desearás codificar las contraseñas de los usuarios. Esto se logra fácilmente asignando la clase Usuario a una de las varias integradas en encoders. Por ejemplo, para almacenar los usuarios en memoria, pero ocultar sus contraseñas a través de sha1, haz lo siguiente:

  • YAML
    # 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
    
  • XML
    <!-- 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>
    
  • PHP
    // 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,
            ),
        ),
    ));
    

Al establecer las iterations a 1 y encode_as_base64 en false, la contraseña simplemente se corre una vez a través del algoritmo sha1 y sin ninguna codificación adicional. Ahora puedes calcular el hash de la contraseña mediante programación (por ejemplo, hash('sha1', 'ryanpass')) o a través de alguna herramienta en línea como functions-online.com

Si vas a crear tus usuarios dinámicamente (y almacenarlos en una base de datos), puedes utilizar algoritmos hash aún más difíciles y, luego confiar en un objeto codificador de clave real para ayudarte a codificar las contraseñas. Por ejemplo, supongamos que tu objeto usuario es Acme\UserBundle\Entity\User (como en el ejemplo anterior). Primero, configura el codificador para ese usuario:

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

En este caso, estás utilizando el algoritmo SHA512 fuerte. Además, puesto que hemos especificado simplemente el algoritmo (sha512) como una cadena, el sistema de manera predeterminada revuelve tu contraseña 5000 veces en una fila y luego la codifica como base64. En otras palabras, la contraseña ha sido fuertemente ofuscada por lo tanto la contraseña revuelta no se puede decodificar (es decir, no se puede determinar la contraseña desde la contraseña ofuscada).

Nuevo en la versión 2.2: A partir de Symfony 2.2 también puedes utilizar los codificadores de contraseña PBKDF2 y BCrypt.

Determinando la contraseña codificada

Si tienes algún formulario de registro para los usuarios, necesitas poder determinar el algoritmo de codificación utilizado en la contraseña, para que lo puedas usar en tu usuario. No importa qué algoritmo configures para tu objeto usuario, 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);

Recuperando el objeto usuario

Después de la autenticación, el objeto Usuario del usuario actual se puede acceder a través del servicio security.context. Desde el interior de un controlador, este se verá así:

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

En un controlador existe un atajo para esto:

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

Nota

Los usuarios anónimos técnicamente están autenticados, lo cual significa que el método isAuthenticated() de un objeto usuario anónimo devolverá true. Para comprobar si el usuario está autenticado realmente, verifica el rol IS_AUTHENTICATED_FULLY.

En una plantilla Twig puedes acceder a este objeto a través de la clave app.user, la cual llama al método GlobalVariables::getUser():

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

Usando múltiples proveedores de usuario

Cada mecanismo de autenticación (por ejemplo, la autenticación HTTP, formulario de acceso, etc.) utiliza exactamente un proveedor de usuario, y de forma predeterminada utilizará el primer proveedor de usuario declarado. Pero, si deseas especificar unos cuantos usuarios a través de la configuración y el resto de los usuarios en la base de datos? Esto es posible creando un nuevo proveedor que encadene los dos:

  • YAML
    # 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 }
    
  • XML
    <!-- 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>
    
  • PHP
    // 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 chain_provider, puesto que es el primero especificado. El chain_provider, a su vez, intenta cargar el usuario, tanto el proveedor in_memory cómo USER_DB.

Truco

Si no tienes razones para separar a tus usuarios in_memory de tus usuarios user_db, lo puedes hacer aún más fácil combinando las dos fuentes en un único proveedor:

  • YAML
    # app/config/security.yml
    security:
        providers:
            main_provider:
                memory:
                    users:
                        foo: { password: test }
                entity:
                    class: Acme\UserBundle\Entity\User,
                    property: username
    
  • XML
    <!-- 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>
    
  • PHP
    // 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 el cortafuegos o mecanismos de autenticación individuales para utilizar un proveedor específico. Una vez más, a menos que explícitamente especifiques un proveedor, siempre se utiliza el primer proveedor:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                provider: user_db
                http_basic:
                    realm: "Secured Demo Area"
                    provider: in_memory
                form_login: ~
    
  • XML
    <!-- 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>
    
  • PHP
    // 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 autenticación HTTP, el sistema de autenticación debe utilizar el proveedor de usuario in_memory. Pero si el usuario intenta acceder a través del formulario de acceso, utilizará el proveedor USER_DB (ya que es el valor predeterminado para el servidor de seguridad en su conjunto).

Para más información acerca de los proveedores de usuario y la configuración del cortafuegos, consulta la Referencia de configuración de Security.

Roles

La idea de un «rol» es clave para el proceso de autorización. Cada usuario tiene asignado un conjunto de roles y cada recurso requiere uno o más roles. Si el usuario tiene los roles necesarios, se le concede acceso. En caso contrario se deniega el acceso.

Los roles son bastante simples, y básicamente son cadenas que puedes inventar y utilizar cuando sea necesario (aunque los roles son objetos internos). Por ejemplo, si necesitas comenzar a limitar el acceso a la sección admin del blog de tu sitio web, puedes proteger esa sección con un rol llamado ROLE_BLOG_ADMIN. Este rol no necesita estar definido en ningún lugar —puedes comenzar a usarlo.

Nota

Todos los roles deben comenzar con el prefijo ROLE_ el cual será gestionado por Symfony2. Si defines tus propios roles con una clase Role dedicada (más avanzada), no utilices el prefijo ROLE_.

Roles jerárquicos

En lugar de asociar muchos roles a los usuarios, puedes definir reglas de herencia creando una jerarquía de roles:

  • YAML
    # app/config/security.yml
    security:
        role_hierarchy:
            ROLE_ADMIN:       ROLE_USER
            ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <role id="ROLE_ADMIN">ROLE_USER</role>
        <role id="ROLE_SUPER_ADMIN">ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH</role>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'role_hierarchy' => array(
            'ROLE_ADMIN'       => 'ROLE_USER',
            'ROLE_SUPER_ADMIN' => array('ROLE_ADMIN', 'ROLE_ALLOWED_TO_SWITCH'),
        ),
    ));
    

En la configuración anterior, los usuarios con rol ROLE_ADMIN también tendrán el rol de ROLE_USER. El rol ROLE_SUPER_ADMIN tiene ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH y ROLE_USER (heredado de ROLE_ADMIN).

Cerrando sesión

Por lo general, también quieres que tus usuarios puedan salir. Afortunadamente, el cortafuegos puede manejar esto automáticamente cuando activas el parámetro de configuración logout:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            secured_area:
                # ...
                logout:
                    path:   /logout
                    target: /
        # ...
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall name="secured_area" pattern="^/">
            <!-- ... -->
            <logout path="/logout" target="/" />
        </firewall>
        <!-- ... -->
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'secured_area' => array(
                // ...
                'logout' => array('path' => 'logout', 'target' => '/'),
            ),
        ),
        // ...
    ));
    

Una vez lo hayas configurado en tu cortafuegos, enviar a un usuario a /logout (o cualquiera que sea tu path configurada), desautenticará al usuario actual. El usuario será enviado a la página de inicio (el valor definido por el parámetro target). Ambos parámetros path y target por omisión se configuran a lo que esté especificado aquí. En otras palabras, a menos que necesites personalizarlos, los puedes omitir por completo y abreviar tu configuración:

  • YAML
    logout: ~
    
  • XML
    <logout />
    
  • PHP
    'logout' => array(),
    

Ten en cuenta que no es necesario implementar un controlador para la URL /logout porque el cortafuegos se encarga de todo. Sin embargo, posiblemente necesites crear una ruta para que la puedas utilizar para generar la URL:

Prudencia

A partir de Symfony 2.1 debes tener una ruta que corresponda a la ruta para cerrar la sesión. Sin esta ruta, el cierre de sesión no trabajará.

  • YAML
    # app/config/routing.yml
    logout:
        path:   /logout
    
  • XML
    <!-- app/config/routing.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    
    <routes xmlns="http://symfony.com/schema/routing"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://symfony.com/schema/routing http://symfony.com/schema/routing/routing-1.0.xsd">
    
        <route id="logout" path="/logout" />
    
    </routes>
    
  • PHP
    // app/config/routing.php
    use Symfony\Component\Routing\RouteCollection;
    use Symfony\Component\Routing\Route;
    
    $collection = new RouteCollection();
    $collection->add('logout', new Route('/logout', array()));
    
    return $collection;
    

Una vez que el usuario ha cerrado la sesión, será redirigido a cualquier ruta definida por el parámetro target anterior (por ejemplo, la página principal). Para más información sobre cómo configurar el cierre de sesión, consulta la Referencia para afinar el sistema de seguridad.

Controlando el acceso en plantillas

Si dentro de una plantilla deseas comprobar si el usuario actual tiene un rol, utiliza la función ayudante incorporada:

  • Twig
    {% if is_granted('ROLE_ADMIN') %}
        <a href="...">Delete</a>
    {% endif %}
    
  • PHP
    <?php if ($view['security']->isGranted('ROLE_ADMIN')): ?>
        <a href="...">Delete</a>
    <?php endif; ?>
    

Nota

Si utilizas esta función y no estás en una URL donde haya un cortafuegos activo, se lanzará una excepción. Una vez más, casi siempre es buena idea tener un cortafuegos principal que cubra todas las URL (como hemos mostrado en este capítulo).

Controlando el acceso en controladores

Si deseas comprobar en tu controlador si el usuario actual tiene un rol, utiliza el método isGranted() del contexto de seguridad:

public function indexAction()
{
    // a los usuarios 'admin' les muestra diferente contenido
    if ($this->get('security.context')->isGranted('ROLE_ADMIN')) {
        // ... aquí carga el contenido 'admin'
    }

    // ... aquí carga otro contenido regular
}

Nota

Debe haber un cortafuegos activo o al llamar al método isGranted se producirá una excepción. Ve la nota anterior acerca de las plantillas para más detalles.

Suplantando a un usuario

A veces, es útil poder cambiar de un usuario a otro sin tener que iniciar sesión de nuevo (por ejemplo, cuando depuras o tratas de entender un error que un usuario ve y que no se puede reproducir). Esto se puede hacer fácilmente activando el escucha switch_user del cortafuegos:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                # ...
                switch_user: true
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <!-- ... -->
            <switch-user />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main'=> array(
                // ...
                'switch_user' => true
            ),
        ),
    ));
    

Para cambiar a otro usuario, sólo tienes que añadir una cadena de consulta con el parámetro _switch_user y el nombre de usuario como el valor de la dirección actual:

http://ejemplo.com/somewhere?_switch_user=thomas

Para volver al usuario original, utiliza el nombre de usuario especial _exit:

http://ejemplo.com/somewhere?_switch_user=_exit

Durante la suplantación, el usuario está provisto con una función de rol especial llamada ROLE_PREVIOUS_ADMIN. En una plantilla, por ejemplo, este rol se puede usar para mostrar un enlace para salir de la suplantación:

  • Twig
    {% if is_granted('ROLE_PREVIOUS_ADMIN') %}
        <a href="{{ path('homepage', {_switch_user: '_exit'}) }}">Exit impersonation</a>
    {% endif %}
  • PHP
    <?php if ($view['security']->isGranted('ROLE_PREVIOUS_ADMIN')): ?>
        <a
            href="<?php echo $view['router']->generate('homepage', array('_switch_user' => '_exit') ?>"
        >
            Exit impersonation
        </a>
    <?php endif; ?>
    

Por supuesto, esta función se debe poner a disposición de un pequeño grupo de usuarios. De forma predeterminada, el acceso está restringido a usuarios que tienen el rol ROLE_ALLOWED_TO_SWITCH. El nombre de esta función se puede modificar a través de la configuración role. Para mayor seguridad, también puedes cambiar el nombre del parámetro de consulta a través de la configuración parameter:

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                # ...
                switch_user: { role: ROLE_ADMIN, parameter: _want_to_be_this_user }
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall>
            <!-- ... -->
            <switch-user role="ROLE_ADMIN" parameter="_want_to_be_this_user" />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main'=> array(
                // ...
                'switch_user' => array('role' => 'ROLE_ADMIN', 'parameter' => '_want_to_be_this_user'),
            ),
        ),
    ));
    

Autenticación apátrida

De forma predeterminada, Symfony2 confía en una cookie (la Sesión) para persistir el contexto de seguridad del usuario. Pero si utilizas certificados o autenticación HTTP, por ejemplo, la persistencia no es necesaria ya que están disponibles las credenciales para cada petición. En ese caso, y si no es necesario almacenar cualquier otra cosa entre peticiones, puedes activar la autenticación apátrida (lo cual significa que Symfony2 jamás creará una cookie):

  • YAML
    # app/config/security.yml
    security:
        firewalls:
            main:
                http_basic: ~
                stateless:  true
    
  • XML
    <!-- app/config/security.xml -->
    <config>
        <firewall stateless="true">
            <http-basic />
        </firewall>
    </config>
    
  • PHP
    // app/config/security.php
    $container->loadFromExtension('security', array(
        'firewalls' => array(
            'main' => array('http_basic' => array(), 'stateless' => true),
        ),
    ));
    

Nota

Si utilizas un formulario de acceso, Symfony2 creará una cookie, incluso si estableces stateless a true.

Utilerías

Nuevo en la versión 2.2: Las clases StringUtils y SecureRandom se añadieron en Symfony 2.2

El componente de seguridad de Symfony viene con una colección de agradables utilidades relacionadas con la seguridad. Estas utilidades las usa Symfony, pero también las deberías utilizar si quieres solucionar los problemas a que están destinadas.

Comparando cadenas

El tiempo que tome comparar dos cadenas depende de sus diferencias. Este lo puede utilizar un atacante cuándo las dos cadenas representan una contraseña por ejemplo; Este se conoce como Ataque temporizado.

Internamente, al comparar dos contraseñas, Symfony utiliza un algoritmo de tiempo constante; Puedes utilizar la misma estrategia en tu propio código gracias a la clase Symfony\Component\Security\Core\Util\StringUtils:

use Symfony\Component\Security\Core\Util\StringUtils;

// ¿es igual la contraseña1 a la contraseña2?
$bool = StringUtils::equals($password1, $password2);

Generando aleatoriamente un número seguro

Siempre que necesites generar un número aleatorio seguro, te animamos a utilizar la clase Symfony\Component\Security\Core\Util\SecureRandom de Symfony:

use Symfony\Component\Security\Core\Util\SecureRandom;

$generator = new SecureRandom();
$random = $generator->nextBytes(10);

El método nextBytes() regresa una cadena aleatoria compuesta del número de caracteres pasados como argumento (10 en el ejemplo anterior).

La clase SecureRandom trabaja mejor cuándo está instalado OpenSSL pero cuándo no está disponible, recae en un algoritmo interno, el cual necesita un archivo de semilla para trabajar correctamente. Sólo suministra un nombre de archivo para habilitarlo:

$generator = new SecureRandom('/alguna/ruta/para/guardar/la/semilla.txt');
$random = $generator->nextBytes(10);

Nota

También puedes acceder a una instancia aleatoria segura directamente desde el contenedor de inyección de dependencias de Symfony; Su nombre es security.secure_random.

Palabras finales

La seguridad puede ser un tema profundo y complejo para resolverlo correctamente en tu aplicación. Afortunadamente, el componente de seguridad de Symfony sigue un modelo de seguridad bien probado en torno a la autenticación y autorización. La autenticación, siempre sucede en primer lugar, está a cargo de un cortafuegos, cuyo trabajo es determinar la identidad del usuario a través de varios métodos diferentes (por ejemplo, la autenticación HTTP, formulario de acceso, etc.) En el recetario, encontrarás ejemplos de otros métodos para manejar la autenticación, incluyendo la manera de implementar una funcionalidad «recuérdame» por medio de cookie.

Una vez que un usuario se autentica, la capa de autorización puede determinar si el usuario debe tener acceso a un recurso específico. Por lo general, los roles se aplican a URL, clases o métodos y si el usuario actual no tiene ese rol, se le niega el acceso. La capa de autorización, sin embargo, es mucho más profunda, y sigue un sistema de «voto» para que varias partes puedan determinar si el usuario actual debe tener acceso a un determinado recurso. Para saber más sobre este y otros temas busca en el recetario.

Bifúrcame en GitHub