Symfony 2.0, el libro oficial

13.3. Usando el típico formulario de acceso

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

Para cargar usuarios desde la base de datos, por favor consulta el artículo How to load Security Users from the Database (the Entity Provider). Después ya podrás crear un sistema completo de formularios de acceso que carguen los usuarios desde la base de datos.

Hasta ahora, se ha visto cómo proteger tu aplicación bajo un firewall y cómo hacer que algunas de sus partes exijan un determinado rol para poder acceder. Aunque es muy fácil utilizar la autenticación HTTP de Symfony para pedir el usuario y contraseña a través de una caja del propio navegador, Symfony soporta otros mecanismos de autenticación más avanzados.

En esta sección, vamos a mejorar este proceso permitiendo la autenticación del usuario a través del típico formulario de login.

En primer lugar, añade la opción form_login en la configuración del firewall:

# app/config/security.yml
security:
    firewalls:
        secured_area:
            pattern:    ^/
            anonymous:  ~
            form_login:
                login_path:  /login
                check_path:  /login_check
<?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>
// 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 los valores login_path o check_path (arriba se muestran sus valores por defecto), puedes simplificar al máximo tu configuración:

form_login: ~
<form-login />
'form_login' => array(),

Ahora, cuando el sistema de seguridad inicia el proceso de autenticación, se redirige al usuario a la ruta que muestra el formulario de acceso (por defecto /login). El formulario tienes que crearlo tú mismo a mano, ya que Symfony no lo proporciona. Primero crea dos nuevas rutas: una que muestre el formulario de acceso (es decir, /login) y una que procese el envío del formulario (por ejemplo, /login_check):

# app/config/routing.yml
login:
    pattern:   /login
    defaults:  { _controller: AcmeSecurityBundle:Security:login }
login_check:
    pattern:   /login_check
<!-- 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>
// 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 es necesario que crees el controlador que procesa la URL /login_check ya que el firewall captura y procesa automáticamente cualquier formulario enviado a esa URL. Por tanto, la ruta login_check es realmente opcional, pero se recomienda crearla para utilizarla después en el formulario de login.

Observa que el nombre de la ruta login no importan. Lo realmente importante es que la URL de la ruta (/login) coincida con el valor de la opción login_path, ya que es donde el sistema de seguridad redirige a los usuarios que necesitan autenticarse.

A continuación, crea el controlador que muestra 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();

        // get the login error if there is one
        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(
                // last username entered by the user
                'last_username' => $session->get(SecurityContext::LAST_USERNAME),
                'error'         => $error,
            )
        );
    }
}

No dejes que este controlador te líe. Como veremos en un momento, cuando el usuario envía el formulario, el sistema de seguridad se encarga de procesar automáticamente el envío del formulario. Si el usuario envía un nombre de usuario o contraseña no válidos, este controlador obtiene el mensaje de error del sistema de seguridad y lo muestra al usuario.

En otras palabras, tu te encargas de mostrar el formulario al usuario y los errores 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:

{# 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" />

    {#
        añade lo siguiente si quieres redirigir al usuario a una
        URL concreta después del login (explicado más adelante)
        <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" />

    <!--
        añade lo siguiente si quieres redirigir al usuario a una
        URL concreta después del login (explicado más adelante)
        <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 AuthenticationException. Como puede contener bastante información sobre el error de autenticación (incluyendo información sensible) debes ser muy cuidadoso con ella.

El formulario de login tiene muy pocos requisitos. En primer lugar, el formulario debe ser enviado a la URL /login_check (a ​​través de la ruta login_check), para que el sistema de seguridad intercepte el formulario y lo procese automáticamente. Y en segundo lugar, los campos del formulario se deben llamar exactamente _username y _password (a menos que los cambies con las opciones username_parameter y password_parameter bajo la clave form_login de la configuración de seguridad).

¡Y eso es todo! Cuando envíes el formulario, el sistema de seguridad comprobará automáticamente las credenciales del usuario y, o bien autenticará al usuario o bien lo enviará de nuevo al formulario de login mostrándole el error producido.

Repasemos todo el proceso:

  1. El usuario intenta acceder a un recurso que está protegido.
  2. El firewall inicia el proceso de autenticación redirigiendo al usuario al formulario de acceso (/login).
  3. La página /login muestra el formulario de acceso gracias a la ruta y el controlador creados 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.

Si las credenciales presentadas son correctas, el usuario será redirigido por defecto a la página solicitada originalmente (por ejemplo /admin/foo). Si el usuario ha accedido directamente a la página de login, se le redirige a la portada.

En cualquier caso, puedes personalizar este comportamiento para redirigir al usuario a cualquier URL que quieras. Consulta el artículo How to customize your Form Login para conocer más opciones de personalización del formulario de login.

13.3.1. Evitando los errores más comunes

Cuando crees tu formulario de acceso, ten cuidado de no cometer alguno de los siguientes errores comunes.

1. Crea las rutas correctas

En primer lugar, asegúrate de que has definido las rutas /login y /login_check correctamente y que correspondan a los valores de configuración login_path y check_path. Una mala configuración puede significar que se el usuario sea redirigido a una página de error 404 en lugar de a la página de acceso, o que al enviar el formulario de login no suceda nada (sólo verás el formulario de login una y otra vez).

2. Asegúrate de que la página de inicio de sesión no esté protegida

Además, asegúrate de que la página que contiene el formulario de login no requiere de ningún rol para verla. La siguiente configuración por ejemplo requiere el rol ROLE_ADMIN para todas las URL (incluso para la URL /login), lo que provoca un bucle de redirecciones:

access_control:
    - { path: ^/, roles: ROLE_ADMIN }
<access-control>
    <rule path="^/" role="ROLE_ADMIN" />
</access-control>
'access_control' => array(
    array('path' => '^/', 'role' => 'ROLE_ADMIN'),
),

Si eliminar el control de acceso en la URL /login, el problema se soluciona:

access_control:
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: ROLE_ADMIN }
<access-control>
    <rule path="^/login" role="IS_AUTHENTICATED_ANONYMOUSLY" />
    <rule path="^/" role="ROLE_ADMIN" />
</access-control>
'access_control' => array(
    array('path' => '^/login', 'role' => 'IS_AUTHENTICATED_ANONYMOUSLY'),
    array('path' => '^/', 'role' => 'ROLE_ADMIN'),
),

Además, si el firewall no permite el acceso a los usuarios anónimos, tendrás que crear un firewall especial que permita los usuarios anónimos en la página del formulario de login:

firewalls:
    login_firewall:
        pattern:    ^/login$
        anonymous:  ~
    secured_area:
        pattern:    ^/
        form_login: ~
<firewall name="login_firewall" pattern="^/login$">
    <anonymous />
</firewall>
<firewall name="secured_area" pattern="^/">
    <form_login />
</firewall>
'firewalls' => array(
    'login_firewall' => array(
        'pattern' => '^/login$',
        'anonymous' => array(),
    ),
    'secured_area' => array(
        'pattern' => '^/',
        'form_login' => array(),
    ),
),

3. Asegúrate de que /login_check está detrás de un firewall

A continuación, asegúrate de que la URL de la ruta check_path (por ejemplo /login_check) está detrás del firewall que estás utilizando para tu formulario de acceso (en este ejemplo, el único firewall cubre todas las URL, incluyendo /login_check). Si /login_check no se encuentra bajo ningún firewall, se lanzará una excepción con el siguiente mensaje: Unable to find the controller for path "/login_check".

4. Diferentes firewalls no comparten el mismo contexto de seguridad

Si estás utilizando varios firewalls y el usuario se autentica contra uno de ellos, esto no significa que se haya autenticado en el resto de firewalls.

Los diferentes firewalls son como diferentes sistemas de seguridad. Por este motivo, para la mayoría de las aplicaciones es suficiente con tener un firewalls principal.