Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

AbstractLoginFormAuthenticator y redireccionamiento a la URL anterior

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

Tengo que confesar algo: en nuestro autentificador, ¡hemos hecho demasiado trabajo! Sí, cuando construyes un autenticador personalizado para un "formulario de inicio de sesión", Symfony proporciona una clase base que puede hacer la vida mucho más fácil. En lugar de extender AbstractAuthenticator extiendeAbstractLoginFormAuthenticator:

... lines 1 - 15
use Symfony\Component\Security\Http\Authenticator\AbstractLoginFormAuthenticator;
... lines 17 - 25
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 28 - 95
}

Mantén Command o Ctrl para abrir esa clase. Sí, extiende AbstractAuthenticatory también implementa AuthenticationEntryPointInterface. ¡Genial! Eso significa que podemos eliminar nuestro redundante AuthenticationEntryPointInterface:

... lines 1 - 23
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 28 - 95
}

La clase abstracta requiere que añadamos un nuevo método llamado getLoginUrl(). Dirígete a la parte inferior de esta clase y ve a "Código"->"Generar" -o Command+N en un Mac- y luego a "Implementar métodos" para generar getLoginUrl(). Para el interior, roba el código de arriba... y devuelve $this->router->generate('app_login'):

... lines 1 - 25
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 28 - 91
protected function getLoginUrl(Request $request): string
{
return $this->router->generate('app_login');
}
}

La utilidad de esta clase base es bastante fácil de ver: ¡implementa tres de los métodos por nosotros! Por ejemplo, implementa supports() comprobando si el método es POST y si la cadena getLoginUrl() coincide con la URL actual. En otras palabras, hace exactamente lo mismo que nuestro método supports(). También gestionaonAuthenticationFailure() -almacenando el error en la sesión y redirigiendo de nuevo a la página de inicio de sesión- y también el punto de entrada - start() - redirigiendo, una vez más, a /login.

Esto significa que... oh sí... ¡podemos eliminar código! Veamos: eliminar supports(),onAuthenticationFailure() y también start():

... lines 1 - 25
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 28 - 36
public function supports(Request $request): ?bool
{
return ($request->getPathInfo() === '/login' && $request->isMethod('POST'));
}
... lines 41 - 75
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
$request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception);
return new RedirectResponse(
$this->router->generate('app_login')
);
}
public function start(Request $request, AuthenticationException $authException = null): Response
{
return new RedirectResponse(
$this->router->generate('app_login')
);
}
... lines 91 - 95
}

Mucho más bonito:

... lines 1 - 25
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
private UserRepository $userRepository;
private RouterInterface $router;
public function __construct(UserRepository $userRepository, RouterInterface $router)
{
... lines 33 - 34
}
public function authenticate(Request $request): PassportInterface
{
... lines 39 - 61
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
... lines 66 - 68
}
protected function getLoginUrl(Request $request): string
{
... line 73
}
}

Asegurémonos de que no rompemos nada: vamos a /admin y... ¡perfecto! El método start() nos sigue redirigiendo a /login. Entremos conabraca_admin@example.com, contraseña tada y... ¡sí! Eso también sigue funcionando: nos redirige a la página de inicio... porque eso es lo que estamos haciendo dentro deonAuthenticationSuccess:

... lines 1 - 25
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 28 - 63
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
return new RedirectResponse(
$this->router->generate('app_homepage')
);
}
... lines 70 - 74
}

TargetPathTrait: Redirección inteligente

Pero... si lo piensas... eso no es lo ideal. Ya que en un principio intentaba ir a /admin... ¿no debería el sistema ser lo suficientemente inteligente como para redirigirnos de nuevo allí después de que hayamos entrado con éxito? Sí Y eso es totalmente posible.

Vuelve a cerrar la sesión. Cuando un usuario anónimo intenta acceder a una página protegida como /admin, justo antes de llamar a la función del punto de entrada, Symfony almacena la URL actual en algún lugar de la sesión. Gracias a esto, en onAuthenticationSuccess(), podemos leer esa URL -que se denomina "ruta de destino"- y redirigirla allí.

Para ayudarnos a hacer esto, ¡podemos aprovechar un trait! En la parte superior de la claseuse TargetPathTrait:

... lines 1 - 24
use Symfony\Component\Security\Http\Util\TargetPathTrait;
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
use TargetPathTrait;
... lines 30 - 81
}

Luego, abajo, en onAuthenticationSuccess(), podemos comprobar si se ha almacenado una "ruta de destino" en la sesión. Lo hacemos diciendo si$target = $this->getTargetPath() - pasando la sesión -$request->getSession() - y luego el nombre del cortafuegos, que en realidad tenemos como argumento. Esa es la clave main:

... lines 1 - 26
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 29 - 66
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($target = $this->getTargetPath($request->getSession(), $firewallName)) {
... line 70
}
return new RedirectResponse(
$this->router->generate('app_homepage')
);
}
... lines 77 - 81
}

Esta línea hace dos cosas a la vez: establece una variable $target a la ruta de destino y, en la sentencia if, comprueba si ésta tiene algo. Porque, si el usuario va directamente a la página de inicio de sesión, entonces no tendrá una ruta de destino en la sesión.

Así que, si tenemos una ruta de destino, redirige a ella: return new RedirectResponse($target):

... lines 1 - 26
class LoginFormAuthenticator extends AbstractLoginFormAuthenticator
{
... lines 29 - 66
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
if ($target = $this->getTargetPath($request->getSession(), $firewallName)) {
return new RedirectResponse($target);
}
return new RedirectResponse(
$this->router->generate('app_homepage')
);
}
... lines 77 - 81
}

¡Hecho y listo! Si mantienes Command o Ctrl y haces clic en getTargetPath() para saltar a ese método central, puedes ver que es súper sencillo: sólo lee una clave muy específica de la sesión. Esta es la clave que el sistema de seguridad establece cuando un usuario anónimo intenta acceder a una página protegida.

¡Vamos a probar esto! Ya hemos cerrado la sesión. Dirígete a /admin. Nuestro punto de entrada nos redirige a /login. Pero además, entre bastidores, Symfony acaba de fijar la URL/admin en esa clave de la sesión. Así que cuando nos conectamos ahora con nuestro correo electrónico y contraseña habituales... ¡impresionante! ¡Se nos redirige de nuevo a /admin!

Siguiente: um... seguimos haciendo demasiado trabajo en LoginFormAuthenticator. ¡Maldita sea! Resulta que, a menos que necesitemos algunas cosas especialmente personalizadas, si estás construyendo un formulario de inicio de sesión, puedes omitir por completo la clase del autentificador personalizado y confiar en un autentificador central de Symfony.

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
    }
}