Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

El punto de entrada: invitar a los usuarios a conectarse

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

Vuelve a entrar utilizando abraca_admin@example.com y la contraseña tada. Cuando vamos a/admin, como hemos visto antes, obtenemos "Acceso denegado". Esto se debe a laaccess_control... y al hecho de que nuestro usuario no tiene ROLE_ADMIN.

Pero si lo cambiamos por ROLE_USER -un rol que sí tenemos-, el acceso está garantizado:

security:
... lines 2 - 40
access_control:
- { path: ^/admin, roles: ROLE_USER }
... lines 43 - 44

Y conseguimos ver unos gráficos impresionantes.

Probemos una cosa más. Cerremos la sesión, es decir, vayamos manualmente a /logout. Ahora que no hemos iniciado la sesión, si vamos directamente a /admin: ¿qué debería ocurrir?

Bueno, en este momento, obtenemos una gran página de error con un código de estado 401. Pero... ¡eso no es lo que queremos! Si un usuario anónimo intenta acceder a una página protegida de nuestro sitio, en lugar de un error, queremos ser súper amables e invitarle a iniciar la sesión. Como tenemos un formulario de entrada, significa que queremos redirigir al usuario a la página de entrada.

¡Hola punto de entrada!

Para saber qué hacer cuando un usuario anónimo accede a una página protegida, cada cortafuegos define algo llamado "punto de entrada". El punto de entrada de un cortafuegos es literalmente una función que dice

¡Esto es lo que debemos hacer cuando un usuario anónimo intenta acceder a una página protegida!

Cada autentificador de nuestro cortafuegos puede o no "proporcionar" un punto de entrada. Ahora mismo, tenemos dos autentificadores: nuestro LoginFormAuthenticator personalizado y también el autentificador remember_me:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
custom_authenticator: App\Security\LoginFormAuthenticator
... lines 25 - 27
remember_me:
secret: '%kernel.secret%'
signature_properties: [password]
#always_remember_me: true
... lines 32 - 44

Pero ninguno de ellos proporciona un punto de entrada, por lo que, en lugar de redirigir al usuario a una página... o algo diferente, obtenemos este error genérico 401. Algunos autenticadores incorporados -como form_login, del que hablaremos pronto- sí proporcionan un punto de entrada... y lo veremos.

Hacer de nuestro autentificador un punto de entrada

Pero, de todos modos, ninguno de nuestros autenticadores proporciona un punto de entrada... ¡así que vamos a añadir uno!

Abre nuestro autentificador: src/Security/LoginFormAuthenticator.php. Si quieres que tu autentificador proporcione un punto de entrada, todo lo que tienes que hacer es implementar una nueva interfaz: AuthenticationEntryPointInterface:

... lines 1 - 22
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class LoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 27 - 89
}

Esto requiere que la clase tenga un nuevo método... que en realidad ya tenemos aquí abajo. Se llama start(). Descomenta el método. Luego, dentro, muy simplemente, vamos a redirigir a la página de inicio de sesión. Voy a robar el código de arriba:

... lines 1 - 22
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class LoginFormAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 27 - 83
public function start(Request $request, AuthenticationException $authException = null): Response
{
return new RedirectResponse(
$this->router->generate('app_login')
);
}
}

¡Y listo!

En cuanto un autentificador implemente esta interfaz, el sistema de seguridad lo notará y empezará a utilizarlo. Literalmente, si un usuario anónimo intenta acceder a una página protegida, ahora llamará a nuestro método start()... y le redirigiremos a la página de inicio de sesión.

Observa: ¡refresca! ¡Bum! Nos lleva a la página de inicio de sesión.

Un cortafuegos tiene exactamente UN punto de entrada

Pero hay una cosa importante que hay que entender sobre los puntos de entrada. Cada cortafuegos sólo puede tener uno. Piensa que en el momento en que entramos en /admin como usuario anónimo.... no estamos intentando entrar a través de un formulario de acceso... o a través de un token de la API. Somos verdaderamente anónimos. Por eso, si tuviéramos varios autentificadores que proporcionaran cada uno un punto de entrada, nuestro cortafuegos no sabría cuál elegir. Necesita un punto de entrada para todos los casos.

Ahora mismo, como sólo uno de nuestros dos autentificadores proporciona un punto de entrada, sabe que debe utilizarlo. Pero, ¿y si no fuera así? Veamos qué pasaría. Busca tu terminal y genera un segundo autentificador personalizado:

symfony console make:auth

Crea un autentificador vacío... y llámalo DummyAuthenticator.

¡Qué bonito! Así se creó una nueva clase llamada DummyAuthenticator:

... lines 1 - 2
namespace App\Security;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\AbstractAuthenticator;
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
class DummyAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
// TODO: Implement supports() method.
}
public function authenticate(Request $request): PassportInterface
{
// TODO: Implement authenticate() method.
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
{
// TODO: Implement onAuthenticationSuccess() method.
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
{
// TODO: Implement onAuthenticationFailure() method.
}
// public function start(Request $request, AuthenticationException $authException = null): Response
// {
// /*
// * If you would like this class to control what happens when an anonymous user accesses a
// * protected page (e.g. redirect to /login), uncomment this method and make this class
// * implement Symfony\Component\Security\Http\EntryPoint\AuthenticationEntrypointInterface.
// *
// * For more details, see https://symfony.com/doc/current/security/experimental_authenticators.html#configuring-the-authentication-entry-point
// */
// }
}

Y también actualizó custom_authenticator en security.yaml para utilizar ambas clases personalizadas:

security:
... lines 2 - 16
firewalls:
... lines 18 - 20
main:
... lines 22 - 23
custom_authenticator:
- App\Security\LoginFormAuthenticator
- App\Security\DummyAuthenticator
... lines 27 - 46

En la nueva clase, dentro de supports(), devuelve false:

... lines 1 - 11
class DummyAuthenticator extends AbstractAuthenticator
{
public function supports(Request $request): ?bool
{
return false;
}
... lines 18 - 43
}

No... vamos a convertir esto en un autentificador real.

Si nos detuviéramos ahora mismo... e intentáramos ir a /admin, seguiría utilizando el punto de entrada de LoginFormAuthenticator. Pero ahora implementaAuthenticationEntryPointInterface:

... lines 1 - 10
use Symfony\Component\Security\Http\EntryPoint\AuthenticationEntryPointInterface;
class DummyAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 15 - 38
}

Y luego baja... y descomenta el método start(). Para el cuerpo, sólo dd()un mensaje... porque esto no se ejecutará nunca:

... lines 1 - 12
class DummyAuthenticator extends AbstractAuthenticator implements AuthenticationEntryPointInterface
{
... lines 15 - 34
public function start(Request $request, AuthenticationException $authException = null): Response
{
dd('DummyAuthenticator::start()!');
}
}

Ahora el cortafuegos se dará cuenta de que tenemos dos autentificadores que proporcionan un punto de entrada cada uno. Y así, cuando refresquemos cualquier página, entrará en pánico. El error dice

¡Ejecuta por ti liiiiii!

Oh, espera, en realidad dice

Como tienes varios autentificadores en el cortafuegos "principal", tienes que establecer la entry_point clave a uno de tus autentificadores.

Y nos dice, de forma muy útil, los dos autentificadores que tenemos. En otras palabras: nos hace elegir.

Copia la clave entry_point... y luego, en cualquier lugar del cortafuegos, dientry_point: y apunta al servicio LoginFormAuthenticator:

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

Técnicamente podemos apuntar a cualquier servicio que implementeAuthenticationEntryPointInterface... pero normalmente lo pongo en mi autentificador.

Ahora... si volvemos a /admin.... ¡funciona bien! Sabe que debe elegir el punto de entrada de LoginFormAuthenticator.

Hablando de LoginFormAuthenticator... um... ¡hemos estado haciendo demasiado trabajo dentro de él! Eso es culpa mía - estamos haciendo las cosas de la manera más difícil para... ya sabes... ¡"aprender"! Pero a continuación, vamos a eliminar eso y aprovechar una clase base de Symfony que nos permitirá eliminar un montón de código. También vamos a aprender algo llamado TargetPathTrait: una forma inteligente de redirigir al usuario en caso de éxito.

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