gstreamer0.10-ffmpeg
gstreamer0.10-plugins-good
packages.
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> |
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 | |
{ | |
'/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.
Como verás en un minuto, no vamos a procesar email
y password
en 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 | |
{ | |
'/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 | |
{ | |
'/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.
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 password
de 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 |
¡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.
"Houston: no signs of life"
Start the conversation!
// 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
}
}