Proyecto compartido. En el taller de Intermodular que abre esta semana has trabajado ensayar la recuperación y publicar el incremento. En Servidor continúas la implementación del mismo producto.
Se explica
25 minutos · explicación y demostración
La ampliación del backend ya tiene reglas y permisos. Hoy la conectas al cliente Angular del proyecto. Angular se trabaja con los conocimientos del módulo de cliente; aquí observaremos el contrato HTTP, el envío de identidad y las respuestas de error del servidor.
El salto de Bruno al Navegador: La barrera de CORS
Durante todo el curso has probado tus endpoints con herramientas de escritorio como Bruno o curl. En ese entorno no existe ninguna restricción de origen cruzado.
Sin embargo, cuando el usuario abre la aplicación en Chrome o Firefox:
- El código frontend de Angular se sirve desde el origen
http://localhost:4200. - La API de Spring Boot escucha en el origen
http://localhost:8080. - Como los puertos difieren, el navegador activa la Política del Mismo Origen (Same-Origin Policy).
Antes de enviar una petición destructiva (POST, PUT, DELETE) con cabeceras personalizadas (Authorization: Bearer), el navegador envía automáticamente una petición de sondeo previo (Preflight Request) con el método OPTIONS:
- Le pregunta al servidor: «¿Aceptas peticiones desde
http://localhost:4200con la cabeceraAuthorization?». - Si Spring Security no está configurado expresamente para autorizar peticiones
OPTIONS, la rechaza y el navegador bloquea la llamada.
La regla de oro de la integración desacoplada
El backend no sabe ni le importa que el cliente sea Angular, React o una app móvil.
El backend solo responde a contratos HTTP estándar. Angular es solo un cliente más. La suite de pruebas de Bruno sigue siendo el certificador técnico oficial e independiente del backend.
Acordar tipos y errores entre Angular y Spring
El cliente Angular consume JSON, no entidades Java. Su interfaz TypeScript debe corresponder al DTO publicado: nombre de los campos, valores opcionales, fechas y estructura de errores. Que ambos proyectos compilen no demuestra que ese acuerdo se cumpla; la comprobación es la petición real y su respuesta.
La URL base pertenece a la configuración del entorno. Las credenciales se añaden de acuerdo con la estrategia implantada y el servidor sigue comprobando cada permiso. Ocultar un botón mejora la experiencia, pero el endpoint también debe rechazar la operación cuando se envía directamente desde un cliente HTTP sin autorización.
La práctica conecta el cliente de Desarrollo Web en Entorno Cliente al mismo backend desplegado. Se observa en DevTools qué ruta se pidió, qué credencial se envió y qué respondió la API. Se prueban un caso válido, un rechazo por permisos y una validación de formulario. La colección HTTP se mantiene para diagnosticar si el problema está en Angular o en el backend.
Se trabaja
140 minutos · implementación guiada sobre el proyecto propio
Paso 1 · Retomar el proyecto y preparar la comprobación
- Abre el cliente Angular existente y el backend y arráncalos siguiendo sus README. Si la integración del cliente aún no está preparada en el otro módulo, deja identificada esa dependencia y utiliza la colección para comprobar mientras tanto el contrato del servidor.
- Localiza el servicio HTTP de Angular, sus tipos de datos y el interceptor de autenticación. Compara sus campos con los DTO públicos actuales.
- Anota el origen del cliente y la dirección del backend que usa la configuración. Abre Red antes de ejecutar la primera acción.
Paso 2 · Configuración de CORS y sincronización de contratos
Para activar el interceptor funcional del ejemplo en un cliente Angular standalone, abre src/app/app.config.ts, importa los siguientes símbolos y añade el proveedor a su array providers existente, conservando router y los otros proveedores. Si ya existe provideHttpClient, modifica esa llamada en lugar de duplicarla.
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth.interceptor';
provideHttpClient(withInterceptors([authInterceptor]))
Reutiliza el AuthService del cliente Angular trabajado en Desarrollo Web en Entorno Cliente: obtenerToken() debe devolver el token actual o null. Si su método tiene otro nombre, adapta esa llamada. Intermodular reutiliza la integración resultante para publicarla. En Red comprueba que Authorization se añade únicamente a peticiones de tu API; si el cliente consume otras APIs, acota el interceptor al prefijo base de tu backend.
Conserva la SecurityFilterChain final de la UD9 y modifica solo orígenes y cabeceras necesarias para Angular. En el cliente, crea o actualiza los tipos en src/app/models y el interceptor en src/app/interceptors; registra el interceptor en provideHttpClient(withInterceptors([...])) de su configuración de arranque, o en el mecanismo equivalente si utiliza módulos. Sin ese registro el archivo no se ejecuta. Comprueba en Red que realmente se envía Authorization antes de diagnosticar permisos del servidor.
Qué se evalúa hoy y qué no
Esta sesión es de backend, aunque se vea TypeScript. Lo que se evalúa es que tu API se deje consumir desde un navegador con seguridad puesta: CORS acotado, cabeceras expuestas, errores legibles. El cliente Angular es el instrumento de medida, no el entregable.
La regla de la UD8 sigue vigente y es la que te salva si Angular no está listo: el backend debe poder comprobarse entero sin él. Si algo no funciona, la primera pregunta es siempre si la misma petición funciona desde tu cliente HTTP. Si desde ahí va y desde el navegador no, el problema es CORS. Si no va desde ninguno de los dos, el problema no tiene nada que ver con Angular.
Configuramos de forma granular los orígenes y cabeceras permitidas en SecurityConfig:
Sustituye solo el bean corsConfigurationSource de SecurityConfig por este método. Conserva la cadena JWT, sus reglas, respuestas 401/403 y demás beans de la sesión 40. Los imports de CorsConfiguration, CorsConfigurationSource, UrlBasedCorsConfigurationSource y List ya estaban en aquella clase.
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// Origen del cliente Angular de desarrollo
configuration.setAllowedOrigins(List.of("http://localhost:4200"));
// Métodos HTTP permitidos
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
// Cabeceras permitidas en las peticiones entrantes
configuration.setAllowedHeaders(List.of(
"Authorization", "Content-Type", "X-Correlation-ID", "Accept"
));
// Cabeceras expuestas legibles por el código JavaScript de Angular
configuration.setExposedHeaders(List.of(
"Location", "X-Correlation-ID", "Content-Disposition"
));
// Permitir envío de credenciales/cookies si fuera necesario
configuration.setAllowCredentials(true);
// Tiempo de caché del resultado del preflight (1 hora)
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", configuration);
return source;
}
Creamos las interfaces en Angular espejando los DTOs de Java:
// src/app/models/proyecto.model.ts
export interface ProyectoResponse {
id: number;
codigo: string;
nombre: string;
estado: 'PLANIFICADO' | 'EN_CURSO' | 'BLOQUEADO' | 'FINALIZADO' | 'CANCELADO';
presupuestoTotal: number;
responsableNombre: string;
}
export interface CrearProyectoRequest {
codigo: string;
nombre: string;
descripcion?: string;
presupuestoTotal: number;
latitud: number;
longitud: number;
fechaInicio: string; // Formato ISO "YYYY-MM-DD"
fechaFinEstimada: string;
}
// Estructura de error estándar RFC 7807 capturada en el frontend
export interface ProblemDetails {
type?: string;
title: string;
status: number;
detail: string;
instance?: string;
correlationId?: string;
}
// src/app/interceptors/auth.interceptor.ts
import { HttpInterceptorFn, HttpErrorResponse } from '@angular/common/http';
import { inject } from '@angular/core';
import { catchError, throwError } from 'rxjs';
import { AuthService } from '../services/auth.service';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
const token = authService.obtenerToken();
// Si tenemos token JWT en localStorage, lo clonamos en la cabecera Authorization
let peticionClonada = req;
if (token) {
peticionClonada = req.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
return next(peticionClonada).pipe(
catchError((error: HttpErrorResponse) => {
// Capturamos el Problem Details RFC 7807 emitido por Spring Boot
if (error.error && error.error.detail) {
console.error(`[API Error ${error.status}] ${error.error.title}: ${error.error.detail}`);
if (error.error.correlationId) {
console.warn(`Código de soporte para reporte: ${error.error.correlationId}`);
}
}
return throwError(() => error);
})
);
};
- Por qué CORS se configura aquí y no en el
@CrossOriginde la UD8 - Con Spring Security en medio, la petición se rechaza en la cadena de filtros antes de llegar a tu controlador, y una anotación en el controlador ya no llega a tiempo. El
.cors(...)delSecurityFilterChainlo resuelve en el sitio correcto: en la frontera. - El
OPTIONStiene que ser público - El navegador envía el preflight sin la cabecera
Authorization. Si tu cadena exige autenticación para todo, eseOPTIONSrecibe un401, el navegador cancela y en la consola verás un error de CORS que no es de CORS. Spring Security lo permite solo si.cors(...)está declarado antes de las reglas de autorización. setExposedHeaders: la que se olvida siempre- Por defecto, JavaScript solo puede leer un puñado de cabeceras de la respuesta. Tu
Locationdel201 Createdllega, pero el navegador se la esconde a Angular salvo que la declares aquí. El síntoma es desconcertante: la petición sale bien y aun así el cliente no encuentra la cabecera. allowCredentials(true)y el comodín- Con credenciales activadas, el estándar prohíbe
setAllowedOrigins(List.of("*")). Spring lanza una excepción al arrancar. Si se requieren varios orígenes, enuméralos o empleasetAllowedOriginPatterns. En producción, el comodín queda descartado en todo caso.
Paso 3 · Inspección de red en DevTools
-
Arranca el backend (
:8080) y el cliente Angular (:4200). -
Abre las herramientas de desarrollo de Chrome (F12) en la pestaña Network (Red).
-
Inicia sesión y crea un proyecto desde el formulario web de Angular:
- Observa la secuencia de dos peticiones en la lista de red:
OPTIONS /api/v1/proyectos: Responde200 OKcon cabeceraAccess-Control-Allow-Origin: http://localhost:4200.POST /api/v1/proyectos: Responde201 Createdcon el cuerpo JSON del proyecto y la cabeceraLocation.
- Observa la secuencia de dos peticiones en la lista de red:
-
Prueba de captura de error RFC 7807 en pantalla:
- Intenta introducir un código duplicado o un presupuesto de -500 €.
- Comprueba que la pantalla de Angular muestra el mensaje amigable extraído directamente de
error.error.detaily el identificador de correlación para soporte.
-
Provoca el fallo de CORS a propósito, para saber reconocerlo: cambia
setAllowedOriginsahttp://localhost:9999, reinicia el backend y repite la operación desde Angular. Lee el mensaje exacto de la consola del navegador y anótalo. Comprueba a la vez que la misma petición sigue funcionando desde tu cliente HTTP: esa asimetría es la firma inconfundible de un problema de CORS y te ahorrará horas el día que aparezca de verdad. Devuelve el origen a:4200.
Paso 4 · Si algo no sale como dice el guion
| Síntoma en la consola del navegador | Causa real | Qué mirar |
|---|---|---|
No 'Access-Control-Allow-Origin' header is present |
El origen no está en la lista | ¿http://localhost:4200 exacto? El puerto y el esquema forman parte del origen |
Response to preflight request doesn't pass access control check: 401 |
El OPTIONS está siendo autenticado |
.cors(...) debe ir declarado en el SecurityFilterChain, antes de las reglas |
Request header field authorization is not allowed |
Falta en setAllowedHeaders |
Añade Authorization a la lista |
La petición va bien pero Angular no ve la cabecera Location |
Falta en setExposedHeaders |
Es lo que impide leerla desde JavaScript |
Cannot use wildcard in Access-Control-Allow-Origin when credentials flag is true |
Comodín + credenciales | Enumera los orígenes o usa setAllowedOriginPatterns |
401 en todas las llamadas de Angular pero no en el cliente HTTP |
El interceptor no adjunta el token | Comprueba en DevTools → Network → Headers que sale Authorization: Bearer … |
| Funciona todo salvo la subida de ficheros | El interceptor fija Content-Type: application/json |
En un multipart, el navegador debe poner él el Content-Type con su boundary: no lo sobrescribas |
Paso 5 · Cerrar el circuito completo desde el navegador
Conecta una operación del cliente cada vez: consulta de detalle, modificación, adjunto e integración externa. Para cada una compara su petición con la que ya funciona en la colección y corrige URL, campos o cabeceras antes de pasar a la siguiente. Un archivo multipart lo construye FormData: deja que el navegador añada Content-Type con su boundary. Tras una respuesta 204 no intentes analizar JSON. Conserva los mensajes de validación y permisos que devuelve el backend.
- Añade al detalle de proyecto un botón «Consultar meteorología en obra» que muestre la temperatura y la recomendación que devuelve tu API.
- Si el backend responde con el aviso de degradación de la UD10 («Servicio no disponible»), muestra una alerta amarilla sin romper la vista del proyecto. Es la demostración visible de que la degradación elegante servía para algo.
- Comprueba desde el navegador las tres respuestas de error que más cuesta ver bien: un
400de validación (envía un presupuesto negativo), un403de permisos (entra como operario e intenta editar un proyecto ajeno) y un404. Las tres deben mostrar eldetaildel RFC 7807 y ninguna debe dejar la pantalla en blanco. - Verifica que la cabecera
Locationdel201 Createdse lee desde Angular y la usas para navegar al recurso recién creado. Si no la ves, vuelve asetExposedHeaders. - Repite el recorrido con los tres roles y comprueba que la interfaz oculta lo que el usuario no puede hacer y que el backend lo rechaza igualmente si lo fuerzas desde el cliente HTTP. Esa doble comprobación es la lección de la sesión 38 aplicada a tu propio proyecto.
- Anota en la memoria técnica los orígenes permitidos y por qué esos: es una decisión de seguridad y hay que defenderla en la sesión 52.
- Cómo saber que lo has terminado
- En DevTools ves el par
OPTIONS 200+POST 201; Angular lee la cabeceraLocation; los errores del backend llegan a la pantalla como texto legible y no como una pantalla en blanco; ninguna operación depende del cliente para poder comprobarse; y sabes reconocer un fallo de CORS por el hecho de que tu cliente HTTP sí funciona.
Paso 6 · Comprobar y registrar el resultado del proyecto
- Ejecuta desde Angular el recorrido de la ampliación y contrasta URL, método, cuerpo, identidad y respuesta con la colección.
- Prueba un dato inválido y un usuario sin permiso: la interfaz debe mostrar el error correspondiente y el backend debe conservar el estado correcto.
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 · Descarga de ficheros binarios Blob en Angular
La descarga de un archivo binario mediante un enlace <a> tradicional no permite inyectar cabeceras Authorization: Bearer <token>:
- Investiga cómo descargar el archivo mediante Angular
HttpClientconfigurando{ responseType: 'blob' }. - Crea una URL de objeto en memoria con
window.URL.createObjectURL(blob)y dispara la descarga programática asignando el nombre de fichero extraído de la cabeceraContent-Disposition.
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.
http://localhost:4200.Content-Disposition.Ver respuestas
1 · Porque las restricciones de CORS son implementadas exclusivamente por los navegadores web para proteger a los usuarios de peticiones no autorizadas entre sitios; los clientes de escritorio como Bruno no aplican la política Same-Origin.
2 · La cabecera Access-Control-Allow-Origin (por ejemplo: Access-Control-Allow-Origin: http://localhost:4200).
3 · Porque por defecto el navegador oculta a JavaScript casi todas las cabeceras de respuesta excepto las básicas; si necesitas leer Location, Content-Disposition o X-Correlation-ID en TypeScript debes exponerlas explícitamente.
4 · El correlationId generado por el servidor, que permite al usuario comunicar ese código al soporte técnico para que localicen el fallo exacto en los archivos de log del servidor.
Cierre
15 minutos · resultado comprobable y explicación individual
Al terminar la sesión:
El cliente Angular utiliza la misma API que sigue verificándose de manera independiente con la colección HTTP.
Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.