Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado integrar el cliente ya construido en servidor. En Servidor continúas la implementación del mismo producto.
Se explica
25 minutos · explicación y demostración
Hasta ahora las operaciones no distinguen quién las solicita. Autenticar es comprobar identidad; autorizar es decidir qué puede hacer esa identidad. Una sesión conserva información del usuario en el servidor y una cookie permite al navegador enviar su identificador. Hoy experimentarás con ese mecanismo y definirás permisos.
La amnesia congénita del protocolo HTTP
Cuando envías una petición GET /api/v1/proyectos, el servidor abre un socket TCP, lee la petición, consulta la base de datos, envía el JSON de respuesta y cierra o reutiliza la conexión.
Si un milisegundo después envías POST /api/v1/proyectos, para el servidor eres un perfecto desconocido. HTTP no tiene memoria: no sabe si eres el mismo usuario que acaba de consultar los proyectos, si estás en la misma oficina o si eres un bot al otro lado del planeta.
El principio sin estado de HTTP
Cada petición HTTP es tratada como si fuera la primera y la única de la historia del universo.
Para construir experiencias web donde un usuario «inicia sesión» y realiza múltiples acciones consecutivas, tuvimos que inventar un mecanismo artificial de continuidad sobre un protocolo amnésico: las sesiones y las cookies.
El truco del guardarropa: Session ID y Cookies
Imagina que vas al guardarropa de un teatro:
- Entregas tu abrigo (haces login con usuario y contraseña).
- El empleado no memoriza tu cara: te entrega una ficha de plástico numerada (un identificador de sesión o Session ID, ej:
ficha #4815). Tu abrigo se queda colgado en el armario del guardarropa (memoria del servidor). - Cada vez que pides algo (una copa, acceso a la sala), muestras la ficha. El personal comprueba el número y te atiende.
- Si pierdes la ficha, o si el guardarropa arde en llamas, se pierde el vínculo.
- 1. Petición inicial (sin cookie)
- 2. Tomcat crea HttpSession (ID: ABC)
- 3. Respuesta con Set-Cookie: JSESSIONID=ABC
- 4. Siguiente petición con Cookie: JSESSIONID=ABC
Las directivas de seguridad indispensables de una cookie
Una cookie no es un simple par clave-valor; es un mensaje con instrucciones de seguridad estrictas para el navegador:
| Directiva | Qué hace | Por qué es obligatoria en producción |
|---|---|---|
HttpOnly |
Impide que el código JavaScript de la página (mediante document.cookie) pueda leer o modificar la cookie. |
Si tu aplicación sufre una vulnerabilidad XSS (inyección de script malicioso), el atacante no puede robar la cookie de sesión. |
Secure |
Obliga al navegador a transmitir la cookie únicamente a través de conexiones cifradas HTTPS. | Evita que un atacante en una red Wi-Fi pública intercepte la cookie en texto claro. |
SameSite=Lax / Strict |
Controla si la cookie se envía en peticiones originadas desde sitios web de terceros. | Es la defensa nativa de los navegadores modernos contra ataques de falsificación de peticiones en sitios cruzados (CSRF). |
Path=/ |
Delimita qué rutas del servidor tienen acceso a la cookie. | Evita que aplicaciones aisladas bajo el mismo dominio compartan identificadores de sesión. |
Los dos pilares de la seguridad: AuthN y AuthZ
En conversaciones informales es muy habitual escuchar a desarrolladores mezclar estos dos conceptos. En ingeniería de software profesional, confundirlos es la puerta de entrada a fallos críticos de seguridad:
- Petición entrante
- 1. Autenticación (AuthN): ¿Quién eres?
- 2. Autorización (AuthZ): ¿Tienes permiso?
- Ejecución del Controlador
- Autenticación (AuthN - Authentication): Es el proceso de verificar la identidad de un actor.
- Responde a la pregunta: ¿Quién eres tú y cómo demuestras que eres quien dices ser?
- Mecanismos: Usuario y contraseña, biometría, tarjeta inteligente, token criptográfico firmado.
- Autorización (AuthZ - Authorization): Es el proceso de determinar si una identidad confirmada tiene permiso para ejecutar una acción sobre un recurso.
- Responde a la pregunta: ¿Este usuario autenticado tiene derecho a ejecutar
DELETE /api/v1/proyectos/1? - Mecanismos: Roles (
ROLE_ADMINISTRADOR), permisos puntuales (TAREA_EDITAR), listas de control de acceso (ACL).
- Responde a la pregunta: ¿Este usuario autenticado tiene derecho a ejecutar
La regla de oro de la precedencia
La autorización no tiene sentido sin una autenticación previa.
No puedes decidir qué permisos tiene alguien si no sabes quién es. Primero se identifica al actor (AuthN); si la identidad es válida, se evalúan sus permisos (AuthZ).
La distinción semántica entre 401 y 403
Uno de los errores más frecuentes en APIs REST es devolver el código HTTP equivocado ante accesos denegados:
| Código HTTP | Nombre formal | Significado real | Cuándo se devuelve |
|---|---|---|---|
401 |
Unauthorized |
En realidad significa Unauthenticated (No identificado o credenciales no válidas). | El cliente no ha enviado ninguna credencial, o el token/contraseña que envió está caducado o es incorrecto. El cliente puede reintentar la petición aportando credenciales válidas. |
403 |
Forbidden |
Identificado pero sin privilegios suficientes (Prohibido). | El servidor sabe perfectamente quién es el usuario (está autenticado), pero sus roles o permisos no le permiten realizar esa operación concreta. Reintentar con las mismas credenciales no servirá de nada. |
Diseño de la Matriz de Control de Acceso (RBAC)
Para que el desarrollo de la seguridad no sea caótico, el equipo de ingeniería define una Matriz RBAC (Role-Based Access Control) antes de escribir código de seguridad:
Definimos los 4 roles del sistema. Son los nombres que usará el resto del curso, del enum Rol que escribes dentro de un momento hasta el proyecto final, así que conviene fijarlos aquí y no volver a tocarlos:
ANON: visitante sin autenticar. No constituye un rol de la aplicación, sino la ausencia de credenciales.DESARROLLADOR: miembro técnico del equipo.JEFE_PROYECTO: responsable de planificación y asignación.ADMINISTRADOR: administrador global de la plataforma.
| Endpoint | Método HTTP | ANON |
DESARROLLADOR |
JEFE_PROYECTO |
ADMINISTRADOR |
|---|---|---|---|---|---|
/api/v1/proyectos |
GET (Listar) |
401 | 200 OK | 200 OK | 200 OK |
/api/v1/proyectos/{id} |
GET (Detalle) |
401 | 200 OK | 200 OK | 200 OK |
/api/v1/proyectos |
POST (Crear) |
401 | 403 | 201 Created | 201 Created |
/api/v1/proyectos/{id} |
PUT (Editar) |
401 | 403 | 200 OK | 200 OK |
/api/v1/proyectos/{id} |
DELETE (Borrar) |
401 | 403 | 403 | 204 No Content |
/api/v1/proyectos/{id}/tareas |
GET (Tareas) |
401 | 200 OK | 200 OK | 200 OK |
/api/v1/proyectos/{id}/tareas |
POST (Crear tarea) |
401 | 201 Created | 201 Created | 201 Created |
/api/v1/tareas/{id} |
DELETE (Borrar tarea) |
401 | 403 | 204 No Content | 204 No Content |
/api/v1/usuarios |
GET (Listar usuarios) |
401 | 403 | 403 | 200 OK |
Se trabaja
140 minutos · implementación guiada sobre el proyecto propio
Paso 1 · Retomar el proyecto y preparar la comprobación
- Abre el cliente integrado y el backend. Reproduce una operación que actualmente puede ejecutar cualquiera.
- Enumera perfiles de tu producto y acciones sobre cada recurso. Añade si importa quién es su propietario; esa será la matriz de permisos.
- Localiza el controlador de diagnóstico y la pestaña de cookies del navegador. Los ejemplos con HttpSession sirven para entender el mecanismo, no para sustituir después Spring Security por un login casero.
Paso 2 · Experimentar con HttpSession en Spring Boot
Crea controller/SesionDemoController.java como experimento separado de tus endpoints de negocio. Abre la URL directamente en el navegador y registra el contador antes de llamar desde otro origen. Después aplica las propiedades de cookie y repite. El POST de cierre se envía con el cliente HTTP o mediante fetch; escribir su URL en la barra envía GET. Este controlador explica HttpSession, no autentica a nadie: se retira del producto cuando incorpores el acceso real.
package com.ejemplo.gestor.controller;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/sesion-demo")
public class SesionDemoController {
@GetMapping("/visita")
public ResponseEntity<Map<String, Object>> registrarVisita(HttpSession session) {
// Obtenemos el contador de la memoria de sesión del servidor
Integer visitas = (Integer) session.getAttribute("contadorVisitas");
if (visitas == null) {
visitas = 1;
} else {
visitas++;
}
// Guardamos el nuevo valor en la sesión
session.setAttribute("contadorVisitas", visitas);
return ResponseEntity.ok(Map.of(
"sessionId", session.getId(),
"numeroVisita", visitas,
"esNuevaSesion", session.isNew(),
"creadaEn", session.getCreationTime()
));
}
@PostMapping("/logout")
public ResponseEntity<Map<String, String>> cerrarSesion(HttpSession session) {
// Destruimos la sesión en la memoria del servidor
session.invalidate();
return ResponseEntity.ok(Map.of("mensaje", "Sesión invalidada con éxito"));
}
}
En Spring Boot podemos forzar que las cookies de sesión cumplan las máximas directivas de seguridad:
# Seguridad de cookies de sesión
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=false
server.servlet.session.cookie.same-site=lax
server.servlet.session.timeout=30m
En local sin HTTPS, en producción con él
En desarrollo local mantenemos server.servlet.session.cookie.secure=false porque trabajamos sobre http://localhost. En producción con certificados TLS/HTTPS, debe ser siempre true.
Paso 3 · Inspeccionar peticiones, respuestas y cookies en DevTools
Arranca tu aplicación y realiza este experimento en tu navegador abriendo http://localhost:8080/api/v1/sesion-demo/visita:
- Primera petición:
- Abre DevTools (
F12) y ve a la pestaña Network. - Revisa en Response Headers la cabecera:
Set-Cookie: JSESSIONID=XXXXX; Path=/; HttpOnly; SameSite=Lax. - En el JSON verás
"numeroVisita": 1y"esNuevaSesion": true.
- Abre DevTools (
- Segunda petición (recarga con F5):
- Mira en Request Headers: el navegador ha enviado automáticamente
Cookie: JSESSIONID=XXXXX. - En el JSON verás
"numeroVisita": 2y"esNuevaSesion": false. ¡El servidor te ha reconocido!
- Mira en Request Headers: el navegador ha enviado automáticamente
- Inspección del almacén de cookies:
- Ve a la pestaña Application (o Almacenamiento en Firefox) → Cookies →
http://localhost:8080. - Comprueba que la cookie
JSESSIONIDtiene el checkbox deHttpOnlymarcado.
- Ve a la pestaña Application (o Almacenamiento en Firefox) → Cookies →
- La prueba de la amnesia provocada:
- Haz clic derecho sobre la cookie
JSESSIONIDen DevTools y selecciona Delete (Borrar). - Recarga la página: el servidor te asigna un nuevo
JSESSIONIDy el contador vuelve a empezar en1.
- Haz clic derecho sobre la cookie
Paso 4 · Si algo no sale como dice el guion
| Síntoma | Causa casi segura | Qué mirar |
|---|---|---|
| El contador vuelve a 1 en cada petición | La cookie no viaja de vuelta | En DevTools → Application → Cookies, comprueba que existe JSESSIONID para localhost:8080 |
| Funciona en el cliente HTTP y no en el navegador | Falta credentials: 'include' |
En una petición a otro origen, el navegador no manda cookies salvo que se lo pidas |
Con credentials: 'include' salta un error de CORS |
El servidor no admite credenciales | allowCredentials(true) y orígenes enumerados, nunca comodín: es la sesión 33 |
| El contador se comparte entre dos pestañas | Es el comportamiento correcto | La sesión es del navegador, no de la pestaña. Para verlo con otra identidad, abre una ventana de incógnito |
| El contador se reinicia al recompilar | La sesión vive en la memoria del proceso | Es exactamente el problema del reto de esta sesión |
Paso 5 · Integrar la cookie con el cliente web de la UD8
Vuelve a tu página cliente/index.html de la UD8:
- Haz un
fetch('http://localhost:8080/api/v1/sesion-demo/visita')desde el cliente enhttp://localhost:5500. - ¡Atención! En peticiones Cross-Origin (distinto puerto), el navegador NO envía cookies por defecto a menos que configures:
fetch('http://localhost:8080/api/v1/sesion-demo/visita', { credentials: 'include' // Obliga a enviar y recibir cookies en peticiones cross-origin }); - Comprueba que el contador de visitas se incrementa en la interfaz web sin recargar la página.
- Localiza la cookie con tus ojos. DevTools → pestaña Application (o Almacenamiento) → Cookies →
http://localhost:8080. Ahí estáJSESSIONIDcon su valor. Anótalo. - Comprueba que la cookie es la sesión, y no otra cosa. Bórrala desde ese mismo panel y vuelve a lanzar la petición: el contador empieza de cero y aparece un
JSESSIONIDnuevo. El servidor no te ha reconocido, aunque eres la misma persona en el mismo ordenador. Eso es lo que significa que HTTP no tiene memoria. - Comprueba que la sesión vive en el servidor. Con el contador en 5, reinicia Spring Boot sin tocar el navegador y vuelve a pedir. Vuelve a 1: la cookie sigue en tu navegador, pero los datos que apuntaba estaban en la memoria del proceso, y el proceso ha muerto. Es el mismo hecho que descubriste en la sesión 1 con las tareas en memoria, aplicado ahora a la identidad.
- Escribe en tres líneas la diferencia entre lo que guarda el navegador (un identificador opaco) y lo que guarda el servidor (los datos asociados). Esa distinción es la que permitirá comprender en la sesión 39 qué cambia al introducir un JWT.
- Cómo saber que lo has terminado
- Has visto la cookie
JSESSIONIDen DevTools, la has borrado y has comprobado el efecto; sabes que reiniciar el servidor pierde la sesión aunque la cookie siga; y puedes explicar por qué la misma petición desde tu cliente HTTP no arrastra estado.
Autenticación frente a autorización
Paso 6 · De la matriz al modelo conceptual en Java
Reutiliza la entidad Usuario que ya represente a los responsables; conserva id, nombre, relaciones y accesos usados por el proyecto. Añade username, hash de contraseña, rol y activo como evolución del modelo, no reemplazando la clase por un esquema incompleto. El enum Rol va en model/Rol.java. Los registros anteriores necesitan una migración antes de imponer nuevas columnas obligatorias: realiza el experimento en la base de desarrollo y prepara sus credenciales con el procedimiento de la sesión 36.
package com.ejemplo.gestor.model;
public enum Rol {
ROLE_DESARROLLADOR,
ROLE_JEFE_PROYECTO,
ROLE_ADMINISTRADOR;
// Prefijo ROLE_ requerido por conveniencia en Spring Security
}
Un usuario tiene credenciales de autenticación (username, password) y una colección de roles para autorización:
@Entity
@Table(name = "usuarios")
public class Usuario {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 50)
private String username;
@Column(nullable = false)
private String password; // ¡Nunca en texto plano! (lo veremos en la sesión 36)
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 30)
private Rol rol;
@Column(nullable = false)
private boolean activo = true;
// Constructores, getters y setters
}
Paso 7 · Auditoría cruzada de la matriz
Revisa la matriz de permisos y responde a estas comprobaciones de diseño:
- El caso del observador anónimo:
- ¿Tiene sentido que la documentación Swagger (
/swagger-ui.html) devuelva401o debería ser pública (200 OK)? - Respuesta: En entornos corporativos las especificaciones internas suelen protegerse; en APIs públicas se dejan abiertas.
- ¿Tiene sentido que la documentación Swagger (
- La coherencia del código de error:
- Si un
DESARROLLADORautenticado intenta borrar un proyecto (DELETE /api/v1/proyectos/5), ¿por qué devolver401sería un grave error técnico? - Respuesta: Porque el usuario ya está autenticado; devolver
401le induciría a pensar que su contraseña falló, cuando en realidad no tiene el privilegio necesario (403 Forbidden).
- Si un
Paso 8 · Control de acceso basado en atributos (ABAC)
La matriz por roles (RBAC) es muy potente, pero tiene un límite:
- Un
DESARROLLADORtiene permiso para editar tareas (PUT /api/v1/tareas/{id}). - Pero, ¿debe poder editar una tarea asignada a otro compañero? ¿O solo las tareas donde él es el responsable?
Diseña la regla de negocio para el control de acceso a nivel de fila (Row-Level Security):
- Añade a la entidad
Tareael campoUsuario asignadoA. - Define la condición lógica: «Un usuario con rol
DESARROLLADORsolo puede modificar el estado de una tarea sitarea.asignadoA.id == usuarioAutenticado.id». - Si intenta editar la tarea de otro desarrollador, ¿qué código HTTP debe responder la API?
- Construye la matriz completa de la que va a depender el resto de la unidad. Una fila por cada combinación de endpoint y método, una columna por rol, y en cada casilla el código de estado exacto. Son las 9 filas de la tabla de esta sesión, y ese documento se convierte en:
- la configuración que escribes en la sesión 36,
- las anotaciones
@PreAuthorizede la sesión 38, - y los tests automáticos de la sesión 38. Si la matriz está mal, la implementación de seguridad construye sobre un error, así que merece la pena discutirla ahora.
- Resuelve las cuatro casillas que siempre generan debate, y anota la razón de cada decisión:
- ¿Un
DESARROLLADORpuede ver los proyectos que no son suyos, aunque no pueda editarlos? - ¿Un
JEFE_PROYECTOpuede borrar un proyecto, o eso solo elADMINISTRADOR? - ¿Alguien que no sea
ADMINISTRADORpuede listar los usuarios del sistema? - ¿La documentación Swagger es pública o exige credenciales?
- ¿Un
- El dilema del 403 frente al 404. Si un usuario pide un recurso que existe pero no le pertenece, un
403le confirma que ese recurso existe. Con identificadores numéricos secuenciales, alguien puede recorrer1, 2, 3…y averiguar cuántos proyectos tiene la empresa sin ver ninguno. Devolver404lo oculta, a costa de un mensaje de error más confuso para el usuario legítimo. Elige una de las dos, aplícala de forma coherente en toda la matriz y déjalo escrito: es una de las preguntas clásicas de la defensa de la UD12.
- Cómo saber que lo has terminado
- Tienes una tabla con todas las casillas rellenas, sin ninguna «depende»; las cuatro decisiones polémicas están tomadas y justificadas por escrito; y has elegido entre
403y404con un criterio que puedes defender.
Paso 9 · Comprobar y registrar el resultado del proyecto
- Observa cuándo se crea y envía la cookie de sesión y qué ocurre al iniciar otro contexto de navegador sin esa cookie.
- Revisa la matriz con otra persona: debe decidir para cada acción si requiere identidad, rol y propiedad, sin dejar casos ambiguos.
Ampliación si has completado el trabajo
Primero termina y verifica los pasos anteriores. Estos retos profundizan en el mismo contenido; no sustituyen la entrega ni obligan a iniciar otro proyecto.
Reto · El coste de la sesión: ¿Qué pasa al escalar a 5 servidores?
El almacenamiento de sesiones en memoria tiene un enemigo mortal: el escalado horizontal.
Imagina que tu empresa tiene tanto éxito que un solo servidor Spring Boot no da abasto y pones un balanceador de carga (Nginx o AWS ALB) delante de dos instancias del backend (Servidor 1 y Servidor 2):
- El usuario hace login en el Servidor 1. El Servidor 1 guarda su sesión en su memoria RAM local y emite
JSESSIONID=ABC. - En la siguiente petición, el balanceador desvía el tráfico al Servidor 2.
- ¿Qué ocurre en el Servidor 2 cuando recibe
JSESSIONID=ABC? ¿Tiene esa sesión en su memoria local? - Investiga las dos soluciones industriales a este problema: Sesiones pegajosas (Sticky Sessions) frente a un Almacén centralizado en memoria (Spring Session con Redis).
Set-Cookie y Cookie inspeccionadas en DevTools.credentials: 'include' y directivas HttpOnly verificadas.Ver respuestas
1 · Porque el protocolo no conserva ninguna información ni contexto entre peticiones sucesivas; cada mensaje se procesa de forma totalmente independiente.
2 · Solo una clave opaca aleatoria (un puntero); los datos reales del usuario (atributos de sesión) permanecen almacenados en la memoria del servidor.
3 · El navegador devuelve una cadena vacía u oculta la cookie, impidiendo que el código JavaScript del cliente acceda a ella y neutralizando el robo de sesión por XSS.
4 · Porque por la Política del Mismo Origen el navegador restringe el envío automático de credenciales a orígenes externos por defecto para evitar fugas involuntarias de sesión.
Reto · Matriz formal de seguridad de la aplicación
En entornos profesionales de desarrollo seguro (como los marcos ISO 27001 o ENS - Esquema Nacional de Seguridad), la seguridad no puede dejarse a la improvisación.
Elabora una matriz formal de control de accesos completa para el sistema:
- Incluye las operaciones sobre las entidades de
Proyecto,Tarea,ComentarioyUsuario. - Define con precisión el código de respuesta HTTP esperado ante cada escenario (éxito, anónimo, rol insuficiente y recurso inexistente).
- Evalúa el compromiso entre seguridad y divulgación de información: ¿debe responderse
403 Forbiddeno404 Not Foundcuando un usuario no tiene permiso para saber siquiera si un recurso existe?
Formato de entrega
Incluye esta explicación en el registro de la sesión dentro del repositorio de GitHub, junto al código y las comprobaciones. La entrega es el enlace al repositorio y al commit de la sesión.
Ver respuestas
1 · Autenticación es verificar la identidad del actor (quién es); autorización es comprobar si esa identidad verificada tiene derecho a ejecutar una acción concreta.
2 · Cuando el usuario ya está autenticado con éxito, pero sus roles o permisos actuales no le otorgan privilegios suficientes para la acción solicitada.
3 · Por una imprecisión histórica en los primeros borradores de la especificación RFC de HTTP en los años 90; conceptualmente el 401 indica falta de credenciales válidas.
4 · Role-Based Access Control: Control de Acceso Basado en Roles.
Cierre
15 minutos · resultado comprobable y explicación individual
Al terminar la sesión:
La matriz incluye usuario anónimo, usuario autenticado, recurso propio y recurso ajeno.
Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.