El problema: HTTP no recuerda
Cada petición HTTP es independiente: el servidor no sabe, por sí solo, que "esta petición es de la misma persona que hizo login hace un minuto". Necesitamos una credencial que el cliente presente en cada llamada. Hay dos grandes familias.
Opción A: sesiones con cookies
- El usuario hace login. El backend crea una sesión en su lado (en memoria, Redis o una base de datos) y genera un id de sesión opaco.
- Ese id viaja al navegador dentro de una cookie. A partir de ahí, el navegador la adjunta automáticamente en cada petición al mismo dominio.
- El backend recibe la cookie, busca la sesión por su id y sabe quién eres.
El estado vive en el servidor; la cookie solo guarda un puntero a él.
Opción B: tokens (JWT)
- Tras el login, el backend firma un token (un JWT) que ya contiene los datos del usuario y lo devuelve en el cuerpo de la respuesta.
- El frontend lo guarda (típicamente en
localStorage) y lo adjunta a mano en cada petición, en la cabeceraAuthorization: Bearer <token>. - El backend verifica la firma del token y confía en su contenido sin consultar ningún almacén: el estado viaja en el propio token (es stateless).
Dónde se guarda y qué riesgos tiene
| Almacén | Lo adjunta | Riesgo principal |
|---|---|---|
Cookie httpOnly |
El navegador, solo | CSRF |
localStorage |
Tu código JS | XSS |
- Una cookie marcada
httpOnlyno es accesible desde JavaScript: aunque haya un fallo de XSS (inyección de código), el atacante no puede leer el token. A cambio, como el navegador la envía sola, es vulnerable a CSRF (que un sitio malicioso provoque peticiones a tu API en tu nombre). localStoragesí es accesible desde JavaScript: si hay XSS, el atacante lee el token y se hace pasar por ti. No sufre CSRF (porque hay que adjuntarlo a mano), pero el robo por XSS es grave.
Anatomía de un JWT
Un JWT son tres partes separadas por puntos, cada una en base64url:
eyJhbG... . eyJzdWIiOiJhbmEi... . 3rXc8f...
cabecera payload (datos) firma
- Cabecera: el algoritmo de firma (p. ej.
HS256). - Payload: los claims (datos):
sub(sujeto),exp(caducidad), rol... - Firma: garantiza que el token no se ha alterado.
⚠️ El payload está codificado, no cifrado: cualquiera puede leerlo. La firma evita que lo modifiquen, pero nunca metas secretos dentro.
Ejemplos
Leer el payload de un JWT sin verificar la firma
const jwt = "cabecera." + btoa(JSON.stringify({ sub: "ana", rol: "admin" })) + ".firma";
const [, payloadB64] = jwt.split(".");
console.log(JSON.parse(atob(payloadB64)));