Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine

Formulario de inicio de sesión API con json_login

Video not working?

It looks like your browser may not support the H264 codec. If you're using Linux, try a different browser or try installing the gstreamer0.10-ffmpeg gstreamer0.10-plugins-good packages.

Thanks! This saves us from needing to use Flash or encode videos in multiple formats. And that let's us get back to making more videos :). But as always, please feel free to message us.

En la página de inicio, que está construida en Vue, tenemos un formulario de inicio de sesión. El objetivo es que, cuando lo enviemos, envíe una petición AJAX con el correo electrónico y la contraseña a una ruta que lo validará.

El formulario en sí está construido aquí en assets/vue/LoginForm.vue:

<template>
<form
v-on:submit.prevent="handleSubmit"
class="book shadow-md rounded px-8 pt-6 pb-8 mb-4 sm:w-1/2 md:w-1/3"
>
... lines 6 - 45
</form>
</template>
<script setup>
import { ref } from 'vue';
... lines 52 - 95
</script>

Si no estás familiarizado con Vue, no te preocupes. Haremos algo de codificación ligera en él, pero lo estoy utilizando principalmente como ejemplo para hacer algunas peticiones a la API.

En la parte inferior, al enviar, hacemos una petición POST a /login enviando los datosemail y password como JSON. Así que nuestro primer objetivo es crear esta ruta:

... lines 1 - 48
<script setup>
... lines 50 - 65
const handleSubmit = async () => {
... lines 67 - 69
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email.value,
password: password.value
})
});
... lines 80 - 93
}
</script>

Crear el controlador de inicio de sesión

Afortunadamente, Symfony tiene un mecanismo incorporado justo para esto. Para empezar, aunque no servirá de mucho, ¡necesitamos un nuevo controlador! En src/Controller/, crea una nueva clase PHP. Llamémosla SecurityController. Parecerá muy tradicional: extiendeAbstractController, luego añade un public function login() que devolverá unResponse, el de HttpFoundation:

... lines 1 - 2
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
... lines 7 - 8
class SecurityController extends AbstractController
{
... line 11
public function login(): Response
{
}
}

Arriba, dale un Route con una URL de /login para que coincida con la que está enviando nuestro JavaScript. Nombra la ruta app_login. Ah, y en realidad no necesitamos hacer esto, pero también podemos añadir methods: ['POST']:

... lines 1 - 6
use Symfony\Component\Routing\Annotation\Route;
class SecurityController extends AbstractController
{
#[Route('/login', name: 'app_login', methods: ['POST'])]
public function login(): Response
{
... line 14
}
}

No habrá una página /login en nuestro sitio a la que hagamos una petición GET: sólo haremos POST a esta URL.

Devolución del ID de usuario actual

Como verás en un minuto, no vamos a procesar email y passworden este controlador... pero esto se ejecutará después de un inicio de sesión correcto. Entonces... ¿qué deberíamos devolver después de un inicio de sesión correcto? No lo sé Y, sinceramente, depende sobre todo de lo que sería útil en nuestro JavaScript. Aún no he pensado mucho en ello, pero quizá... ¿el identificador de usuario? Empecemos por ahí.

Si la autenticación se ha realizado correctamente, entonces, en este punto, el usuario habrá iniciado sesión normalmente. Para obtener el usuario autenticado actualmente, voy a aprovechar una nueva función de Symfony. Añade un argumento con un atributo PHP llamado#[CurrentUser]. Entonces podemos utilizar el tipo-hint normal User, llamarlo $user y por defecto null, en caso de que no estemos logueados por alguna razón:

... lines 1 - 7
use Symfony\Component\Security\Http\Attribute\CurrentUser;
class SecurityController extends AbstractController
{
#[Route('/login', name: 'app_login', methods: ['POST'])]
public function login(#[CurrentUser] $user = null): Response
{
... lines 15 - 17
}
}

Hablaremos de cómo es posible en un minuto.

A continuación, devuelve $this->json() con una clave user establecida en $user->getId():

... lines 1 - 9
class SecurityController extends AbstractController
{
#[Route('/login', name: 'app_login', methods: ['POST'])]
public function login(#[CurrentUser] $user = null): Response
{
return $this->json([
'user' => $user ? $user->getId() : null,
]);
}
}

¡Genial! Y eso es todo lo que necesitamos que haga nuestro controlador.

Activar json_login

Para activar el sistema que hará el verdadero trabajo de leer el correo electrónico y la contraseña, dirígete a config/packages/security.yaml. Debajo del cortafuegos, añade json_login y debajo check_path... que debería estar configurado con el nombre de la ruta que acabamos de crear. Así, app_login:

security:
... lines 2 - 11
firewalls:
... lines 13 - 15
main:
... lines 17 - 18
json_login:
check_path: app_login
... lines 21 - 46

Esto activa una escucha de seguridad: es un trozo de código que ahora vigilará cada petición para ver si es una petición POST a esta ruta. Por tanto, un POST a /login. Si lo es, descodificará el JSON de esa petición, leerá las claves email y passwordde ese JSON, validará la contraseña y nos conectará.

Sin embargo, tenemos que decirle qué claves del JSON estamos utilizando. Nuestro JavaScript está enviando email y password: super creativo. Así que debajo de esto, ponusername_path a email y password_path a password:

security:
... lines 2 - 11
firewalls:
... lines 13 - 15
main:
... lines 17 - 18
json_login:
check_path: app_login
username_path: email
password_path: password
... lines 23 - 48

El proveedor de usuario

¡Listo! Pero, ¡espera! Si enviamos un POST email y password a esta ruta... ¿cómo demonios sabe el sistema cómo encontrar a ese usuario? ¿Cómo se supone que sabe que debe consultar la tabla user WHERE email = el correo electrónico de la petición?

¡Excelente pregunta! En el episodio 1, ejecutamos:

php ./bin/console make:user

Esto creó una entidad User con las cosas básicas de seguridad que necesitamos:

... lines 1 - 38
class User implements UserInterface, PasswordAuthenticatedUserInterface
{
... lines 41 - 43
private ?int $id = null;
... lines 45 - 49
private ?string $email = null;
... lines 51 - 52
private array $roles = [];
... lines 54 - 59
private ?string $password = null;
... lines 61 - 64
private ?string $username = null;
... lines 66 - 187
}

En security.yaml, también creó un proveedor de usuario:

security:
... lines 2 - 4
# https://symfony.com/doc/current/security.html#loading-the-user-the-user-provider
providers:
# used to reload user from session & other features (e.g. switch_user)
app_user_provider:
entity:
class: App\Entity\User
property: email
... lines 12 - 48

Se trata de un proveedor de entidad: indica al sistema de seguridad que busque usuarios en la base de datos consultando por la propiedad email. Esto significa que nuestro sistema descodificará el JSON, obtendrá la clave email, buscará un User con un correo electrónico que coincida y, a continuación, validará la contraseña. En otras palabras... ¡estamos listos!

Volviendo a LoginForm.vue, el JavaScript también está listo: handleSubmit() se llamará cuando enviemos el formulario... y realiza la llamada AJAX:

... lines 1 - 48
<script setup>
... lines 50 - 65
const handleSubmit = async () => {
isLoading.value = true;
error.value = '';
const response = await fetch('/login', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
email: email.value,
password: password.value
})
});
isLoading.value = false;
if (!response.ok) {
const data = await response.json();
console.log(data);
// TODO: set error
return;
}
email.value = '';
password.value = '';
//emit('user-authenticated', userIri);
}
</script>

¡Así que vamos a probarlo! Muévete y actualiza para estar seguro. Pruébalo primero con un correo electrónico y una contraseña falsos. Envías y... ¿no pasa nada? Abre el inspector de tu navegador y ve a la consola. ¡Sí! Ves un código de estado 401 y arroja este error: credenciales no válidas. Eso viene de aquí mismo, de nuestro JavaScript: una vez finalizada la petición, si la respuesta es "no está bien" -lo que significa que había un código de estado 4XX o 5XX-, descodificamos el JSON y lo registramos.

Aparentemente, cuando fallamos la autenticación con json_login, devuelve un pequeño trozo de JSON con "Credenciales no válidas".

A continuación: convirtamos este error en algo que podamos ver en el formulario, gestionemos otro caso de error y luego pensemos qué hacer cuando la autenticación tenga éxito.

Leave a comment!

0
Login or Register to join the conversation
Cat in space

"Houston: no signs of life"
Start the conversation!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=8.1",
        "ext-ctype": "*",
        "ext-iconv": "*",
        "api-platform/core": "^3.0", // v3.1.2
        "doctrine/annotations": "^2.0", // 2.0.1
        "doctrine/doctrine-bundle": "^2.8", // 2.8.3
        "doctrine/doctrine-migrations-bundle": "^3.2", // 3.2.2
        "doctrine/orm": "^2.14", // 2.14.1
        "nelmio/cors-bundle": "^2.2", // 2.2.0
        "nesbot/carbon": "^2.64", // 2.66.0
        "phpdocumentor/reflection-docblock": "^5.3", // 5.3.0
        "phpstan/phpdoc-parser": "^1.15", // 1.16.1
        "symfony/asset": "6.2.*", // v6.2.5
        "symfony/console": "6.2.*", // v6.2.5
        "symfony/dotenv": "6.2.*", // v6.2.5
        "symfony/expression-language": "6.2.*", // v6.2.5
        "symfony/flex": "^2", // v2.2.4
        "symfony/framework-bundle": "6.2.*", // v6.2.5
        "symfony/property-access": "6.2.*", // v6.2.5
        "symfony/property-info": "6.2.*", // v6.2.5
        "symfony/runtime": "6.2.*", // v6.2.5
        "symfony/security-bundle": "6.2.*", // v6.2.6
        "symfony/serializer": "6.2.*", // v6.2.5
        "symfony/twig-bundle": "6.2.*", // v6.2.5
        "symfony/ux-react": "^2.6", // v2.7.1
        "symfony/ux-vue": "^2.7", // v2.7.1
        "symfony/validator": "6.2.*", // v6.2.5
        "symfony/webpack-encore-bundle": "^1.16", // v1.16.1
        "symfony/yaml": "6.2.*" // v6.2.5
    },
    "require-dev": {
        "doctrine/doctrine-fixtures-bundle": "^3.4", // 3.4.2
        "mtdowling/jmespath.php": "^2.6", // 2.6.1
        "phpunit/phpunit": "^9.5", // 9.6.3
        "symfony/browser-kit": "6.2.*", // v6.2.5
        "symfony/css-selector": "6.2.*", // v6.2.5
        "symfony/debug-bundle": "6.2.*", // v6.2.5
        "symfony/maker-bundle": "^1.48", // v1.48.0
        "symfony/monolog-bundle": "^3.0", // v3.8.0
        "symfony/phpunit-bridge": "^6.2", // v6.2.5
        "symfony/stopwatch": "6.2.*", // v6.2.5
        "symfony/web-profiler-bundle": "6.2.*", // v6.2.5
        "zenstruck/browser": "^1.2", // v1.2.0
        "zenstruck/foundry": "^1.26" // v1.28.0
    }
}
userVoice