Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Sistema de recordarme

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

Otra buena característica de un formulario de acceso es la casilla "recuérdame". Aquí almacenamos una cookie "recuérdame" de larga duración en el navegador del usuario, de modo que cuando cierre su navegador -y por tanto, pierda su sesión- esa cookie le mantendrá conectado... durante una semana... o un año... o lo que configuremos. Añadamos esto.

Habilitar el sistema remember_me

El primer paso es ir a config/packages/security.yaml y activar el sistema. Lo hacemos diciendo remember_me: y, a continuación, estableciendo una pieza de configuración necesaria: secret: establecer en %kernel.secret%:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 27
remember_me:
secret: '%kernel.secret%'
... lines 30 - 42

Esto se utiliza para "firmar" el valor de la cookie remember me... y el parámetro kernel.secretviene en realidad de nuestro archivo .env:

28 lines .env
... lines 1 - 15
###> symfony/framework-bundle ###
... line 17
APP_SECRET=c28f3d37eba278748f3c0427b313e86a
###
... lines 20 - 28

Sí, este APP_SECRET acaba convirtiéndose en el parámetro kernel.secret... al que podemos hacer referencia aquí.

Como es normal, hay un montón de otras opciones que puedes poner en remember_me... y puedes ver muchas de ellas ejecutando:

symfony console debug:config security

Busca la sección remember_me:. Una importante es lifetime:, que es el tiempo de validez de la cookie "Recuérdame".

Antes he dicho que la mayor parte de la configuración que ponemos bajo nuestro cortafuegos sirve para activar diferentes autentificadores. Por ejemplo, custom_authenticator:activa nuestro LoginFormAuthenticator:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
custom_authenticator: App\Security\LoginFormAuthenticator
... lines 25 - 42

Lo que significa que ahora se llama a nuestra clase al inicio de cada petición y se busca el envío de un formulario de acceso. La configuración de remember_me también activa un autentificador: un autentificador central llamado RememberMeAuthenticator. En cada petición, éste busca una cookie "recuérdame" -que crearemos en un segundo- y, si está ahí, la utiliza para autenticar al usuario.

Añadir la casilla de verificación "Recuérdame

Ahora que esto está en su sitio, nuestro siguiente trabajo es establecer esa cookie en el navegador del usuario después de que se conecte. Abre login.html.twig. En lugar de añadir siempre la cookie, dejemos que el usuario elija. Justo después de la contraseña, añade un div con algunas clases, una etiqueta y una entrada type="checkbox",name="_remember_me":

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<form method="post" class="row g-3">
... lines 10 - 24
<div class="form-check mb-3">
<label>
<input type="checkbox" name="_remember_me" class="form-check-input"> Remember me
</label>
</div>
... lines 30 - 39
</form>
</div>
</div>
</div>
{% endblock %}

El nombre - _remember_me - es importante y tiene que ser ese valor. Como veremos en un minuto, el sistema busca una casilla de verificación con este nombre exacto.

Bien, actualiza el formulario. Genial, ¡tenemos una casilla de verificación! Aunque... es un poco feo... creo que se ha estropeado algo. Usa form-check y démosle a nuestra casilla de verificaciónform-check-input:

... lines 1 - 4
{% block body %}
<div class="container">
<div class="row">
<div class="login-form bg-light mt-4 p-4">
<form method="post" class="row g-3">
... lines 10 - 24
<div class="form-check mb-3">
<label>
<input type="checkbox" name="_remember_me" class="form-check-input"> Remember me
</label>
</div>
... lines 30 - 39
</form>
</div>
</div>
</div>
{% endblock %}

Ahora... ¡mejor!

Si marcáramos la casilla y la enviáramos... no pasaría absolutamente nada diferente: Symfony no establecería una cookie "Recuérdame".

Esto se debe a que nuestro autentificador necesita anunciar que admite el establecimiento de cookies remember me. Esto es un poco raro, pero piénsalo: el hecho de que hayamos activado el sistema remember_me en security.yaml no significa que queramos que SIEMPRE se establezcan cookies remember me. En un formulario de inicio de sesión, definitivamente. Pero si tuviéramos algún tipo de autenticación con token de la API... entonces no querríamos que Symfony intentara establecer una cookie remember me en esa petición de la API.

En cualquier caso, todo lo que tenemos que añadir es una pequeña bandera que diga que este mecanismo de autenticación sí admite añadir cookies remember me. Hazlo con una insignia: new RememberMeBadge():

... lines 1 - 16
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\RememberMeBadge;
... lines 18 - 23
class LoginFormAuthenticator extends AbstractAuthenticator
{
... lines 26 - 39
public function authenticate(Request $request): PassportInterface
{
... lines 42 - 44
return new Passport(
new UserBadge($email, function($userIdentifier) {
... lines 47 - 55
new PasswordCredentials($password),
[
... lines 58 - 61
new RememberMeBadge(),
]
);
}
... lines 66 - 92
}

¡Eso es todo! Pero hay una cosa rara. Con el CsrfTokenBadge, leemos el token POSTed y se lo pasamos a la insignia. Pero con RememberMeBadge... no hacemos eso. En su lugar, internamente, el sistema "recuérdame" sabe que debe buscar una casilla llamada, exactamente, _remember_me.

Todo el proceso funciona así. Después de que nos autentifiquemos con éxito, el sistema "recuérdame" buscará esta insignia y mirará si esta casilla está marcada. Si ambas cosas son ciertas, añadirá la cookie "recuérdame".

Veamos esto en acción. Actualiza la página... e introduce nuestro correo electrónico normal, la contraseña "tada", haz clic en la casilla "Recuérdame"... y pulsa "Iniciar sesión". La autenticación se ha realizado con éxito No es ninguna sorpresa. Pero ahora abre las herramientas de tu navegador, ve a "Aplicación", busca "Cookies" y... ¡sí! Tenemos una nueva cookie REMEMBERME... que caduca dentro de mucho tiempo: ¡es decir, dentro de 1 año!

Para demostrar que el sistema funciona, elimina la cookie de sesión que normalmente nos mantiene conectados. Observa lo que ocurre cuando actualizamos. ¡Seguimos conectados! Eso es gracias al autentificador remember_me.

Cuando te autentificas, internamente, tu objeto User se envuelve en un objeto "token"... que normalmente no es demasiado importante. Pero ese token muestra cómo te has autentificado. Ahora dice RememberMeToken... lo que demuestra que la cookie "Recuérdame" fue la que nos autenticó.

Ah, y si te preguntas por qué Symfony no ha añadido una nueva cookie de sesión... eso es sólo porque la sesión de Symfony es perezosa. No lo verás hasta que vayas a una página que utilice la sesión - como la página de inicio de sesión. Ahora está de vuelta.

Y... ¡eso es todo! Además de nuestro LoginFormAuthenticator, ahora hay un segundo autentificador que busca información de autentificación en una cookie deREMEMBERME.

Sin embargo, podemos hacer todo esto un poco más elegante. A continuación, vamos a ver cómo podríamos añadir una cookie "Recuérdame" para todos los usuarios cuando se conecten, sin necesidad de una casilla de verificación. También vamos a explorar una nueva opción del sistema "recuérdame" que permite invalidar todas las cookies "recuérdame" existentes si el usuario cambia su contraseña.

Leave a comment!

¡Este tutorial también funciona muy bien para Symfony 6!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": "^7.4.1 || ^8.0.0",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "babdev/pagerfanta-bundle": "^3.3", // v3.3.0
        "composer/package-versions-deprecated": "^1.11", // 1.11.99.4
        "doctrine/annotations": "^1.0", // 1.13.2
        "doctrine/doctrine-bundle": "^2.1", // 2.6.3
        "doctrine/doctrine-migrations-bundle": "^3.0", // 3.1.1
        "doctrine/orm": "^2.7", // 2.10.1
        "knplabs/knp-markdown-bundle": "^1.8", // 1.9.0
        "knplabs/knp-time-bundle": "^1.11", // v1.16.1
        "pagerfanta/doctrine-orm-adapter": "^3.3", // v3.3.0
        "pagerfanta/twig": "^3.3", // v3.3.0
        "phpdocumentor/reflection-docblock": "^5.2", // 5.2.2
        "scheb/2fa-bundle": "^5.12", // v5.12.1
        "scheb/2fa-qr-code": "^5.12", // v5.12.1
        "scheb/2fa-totp": "^5.12", // v5.12.1
        "sensio/framework-extra-bundle": "^6.0", // v6.2.0
        "stof/doctrine-extensions-bundle": "^1.4", // v1.6.0
        "symfony/asset": "5.3.*", // v5.3.4
        "symfony/console": "5.3.*", // v5.3.7
        "symfony/dotenv": "5.3.*", // v5.3.8
        "symfony/flex": "^1.3.1", // v1.17.5
        "symfony/form": "5.3.*", // v5.3.8
        "symfony/framework-bundle": "5.3.*", // v5.3.8
        "symfony/monolog-bundle": "^3.0", // v3.7.0
        "symfony/property-access": "5.3.*", // v5.3.8
        "symfony/property-info": "5.3.*", // v5.3.8
        "symfony/rate-limiter": "5.3.*", // v5.3.4
        "symfony/runtime": "5.3.*", // v5.3.4
        "symfony/security-bundle": "5.3.*", // v5.3.8
        "symfony/serializer": "5.3.*", // v5.3.8
        "symfony/stopwatch": "5.3.*", // v5.3.4
        "symfony/twig-bundle": "5.3.*", // v5.3.4
        "symfony/ux-chartjs": "^1.3", // v1.3.0
        "symfony/validator": "5.3.*", // v5.3.8
        "symfony/webpack-encore-bundle": "^1.7", // v1.12.0
        "symfony/yaml": "5.3.*", // v5.3.6
        "symfonycasts/verify-email-bundle": "^1.5", // v1.5.0
        "twig/extra-bundle": "^2.12|^3.0", // v3.3.3
        "twig/string-extra": "^3.3", // v3.3.3
        "twig/twig": "^2.12|^3.0" // v3.3.3
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.3", // 3.4.0
        "symfony/debug-bundle": "5.3.*", // v5.3.4
        "symfony/maker-bundle": "^1.15", // v1.34.0
        "symfony/var-dumper": "5.3.*", // v5.3.8
        "symfony/web-profiler-bundle": "5.3.*", // v5.3.8
        "zenstruck/foundry": "^1.1" // v1.13.3
    }
}