← Sesión, autenticación y Spring Security

Sesión 36 · Semana 18

Contraseñas y Spring Security

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

Ya has definido quién debería acceder a cada operación. Spring Security aplicará controles de acceso antes del controlador. Un hash de contraseña permite comprobarla sin guardar su texto; BCrypt incorpora sal y un coste de cálculo. Hoy conectarás esas piezas con reglas de acceso explícitas.

Cifrado reversible frente a Hash unidireccional

Conviene fijar con máxima claridad conceptual estos dos términos:

Cifrado reversible vs Hash unidireccional
  1. Texto en claro ("secreto") → Función Hash irreversible → Hash ("$2a$12$...")
  2. Hash ("$2a$12$...") → Imposible recuperar matemáticamente → "secreto"
  • Cifrado (Criptografía reversible): Tiene un camino de ida y vuelta. Si tienes el texto cifrado y la clave secreta, puedes descifrarlo para obtener el texto original. Se usa para transmitir mensajes secretos o guardar números de tarjeta de crédito que luego necesitas cobrar.
  • Hash (Función unidireccional): Solo tiene camino de ida. A partir de una entrada genera una huella digital matemática de longitud fija. Es matemáticamente imposible revertir el hash para obtener la contraseña original.

¿Cómo se comprueba entonces el login si el servidor no conoce la contraseña?

  1. El usuario introduce "MiPassword123".
  2. El servidor le aplica la misma función hash a lo que el usuario acaba de escribir.
  3. Si el hash resultante coincide con el hash guardado en la base de datos, el usuario conoce la contraseña. El servidor jamás necesita saber cuál era la contraseña original.

Por qué SHA-256 no sirve para contraseñas: La necesidad de funciones lentas

Muchos estudiantes preguntan: «¿Por qué no usamos SHA-256 si es un hash seguro?».

SHA-256 es un algoritmo excelente para verificar la integridad de un archivo de 4 GB, porque fue diseñado para ser ultrarrápido.

  • Una tarjeta gráfica (GPU) moderna para videojuegos puede calcular más de 10.000 millones de hashes SHA-256 por segundo.
  • Si un atacante roba tu base de datos con contraseñas en SHA-256, puede probar todas las combinaciones posibles de 8 caracteres en cuestión de minutos (fuerza bruta).

Para almacenar contraseñas necesitamos funciones de derivación de claves deliberadamente lentas y costosas en CPU y memoria: BCrypt, Argon2 o PBKDF2.

  • Si el cálculo de un hash BCrypt tarda 250 milisegundos, un usuario legítimo al hacer login ni siquiera nota el retardo de un cuarto de segundo.
  • Para el atacante, probar 1.000 millones de contraseñas ya no tarda minutos: tarda miles de años.

El rol del Salt (Sal criptográfica) y la anatomía de BCrypt

Para evitar que dos usuarios con la misma contraseña tengan el mismo hash en la base de datos (lo que permitiría usar tablas arcoíris precalculadas), se añade un Salt: un conjunto de bytes aleatorios únicos generados para cada usuario.

BCrypt empaqueta todo en una cadena modular estándar de 60 caracteres:

Anatomía de un hash BCrypt de 60 caracteres
  1. Algoritmo ($2a$)
  2. Coste ($12$)
  3. Salt aleatorio (22 chars)
  4. Hash de la contraseña (31 chars)
$2a$12$KIXQ0zv4pO3mR7uYbA1c.eW9tHnL5sD2fG8jV4xZ6qC1aB3dE5gHi
 |   |  \____________________/\_____________________________/
Id  Cost         Salt                        Hash

Ese ejemplo sirve para ver la estructura, no para copiarlo: cada hash lleva su propia sal, así que el tuyo será distinto aunque la contraseña sea la misma. En la sesión 37 generarás los tuyos.

  1. $2a$: Versión del algoritmo BCrypt.
  2. $12$: Factor de coste (Work Factor). Significa 2¹² = 4.096 rondas de estiramiento de clave (Key Stretching).
  3. Primeros 22 caracteres: El Salt aleatorio generado automáticamente en el momento del registro.
  4. Últimos 31 caracteres: El hash resultante de combinar la contraseña con ese Salt.

El cerrojo automático de Spring Security

En el instante en que añades esta dependencia a tu pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Spring Boot activa el principio de seguridad por defecto (Secure by Default):

  1. Todos los endpoints quedan blindados: Cualquier petición a /api/v1/proyectos o a /swagger-ui.html es inmediatamente rechazada con código 401 Unauthorized.
  2. Genera un usuario provisional: En la terminal de arranque de la aplicación aparece un mensaje como este:
    Using generated security password: 4a8b1c2d-9e3f-4123-b890-abcdef123456
    El usuario por defecto es user y la contraseña es esa clave aleatoria efímera.
  3. Inserta la Cadena de Filtros de Seguridad: Cada petición HTTP entrante es interceptada por una serie de filtros en cascada antes de llegar siquiera al DispatcherServlet.

La arquitectura de filtros

Spring Security no vive dentro de tus controladores; vive en la frontera de red.

El DelegatingFilterProxy desvía la petición a la SecurityFilterChain. Si un filtro detecta que la petición no aporta credenciales válidas, corta la ejecución de inmediato y responde al cliente sin que tu código de negocio llegue a enterarse.

La cadena de filtros (SecurityFilterChain) en Spring Security 6

En versiones antiguas de Spring Security se heredaba de WebSecurityConfigurerAdapter. En Spring Boot 3 esa clase fue eliminada definitivamente.

La configuración moderna se realiza mediante un @Bean que construye un SecurityFilterChain utilizando programación funcional basada en lambdas:

El flujo de inspección de la SecurityFilterChain
  1. Petición HTTP entrante
  2. Filtro CORS (WebConfig)
  3. Filtro CSRF / ExceptionTranslation
  4. BasicAuthenticationFilter
  5. AuthorizationFilter (requestMatchers)
  6. DispatcherServlet / Controller

Se trabaja

140 minutos · implementación guiada sobre el proyecto propio

Paso 1 · Retomar el proyecto y preparar la comprobación

  1. Abre la matriz de permisos, pom.xml y la configuración actual. Guarda una petición pública y otra que deberá quedar protegida.
  2. Localiza el modelo de usuario o prepara el indicado en el ejercicio, diferenciándolo de una entidad de negocio que solo represente a una persona.
  3. Prepara contraseñas ficticias de prueba y anota las rutas públicas de registro o acceso previstas. No utilices contraseñas personales en ejemplos o commits.

Paso 2 · Comprobar por qué no se guardan contraseñas en texto plano

Una filtración de una tabla con contraseñas en texto plano permitiría leerlas directamente. Guardar un hash evita esa lectura, pero un atacante todavía puede probar contraseñas candidatas y comparar resultados. BCrypt incorpora una sal aleatoria y un coste de cálculo para dificultar esas pruebas.

  1. Analiza la seguridad del acceso y dibuja dos recorridos: registro (contraseña → hash → base de datos) y acceso (contraseña recibida + hash guardado → comparación).
  2. Marca qué dato se guarda y qué dato se descarta. El hash también es sensible y no se incluye en las respuestas públicas ni en logs.
  3. En el siguiente paso ejecutarás una prueba con contraseñas ficticias: dos hashes diferentes pueden verificar la misma contraseña. Esa observación explica por qué no se compara encode(entrada).equals(hashGuardado).

Al terminar debes poder explicar qué aporta la sal, qué aporta el coste y por qué sigue siendo necesaria una contraseña difícil de adivinar.

Paso 3 · Integrar BCrypt con Spring Security

Antes de copiar estas clases añade una sola vez spring-boot-starter-security a pom.xml y sincroniza Maven: de ahí proceden PasswordEncoder y BCryptPasswordEncoder. Crea config/SecurityBeansConfig.java y después el test bajo src/test/java. El test instancia el encoder directamente y no necesita arrancar el backend; ejecútalo antes de configurar HTTP. Al reiniciar la aplicación después de añadir seguridad cambiará el acceso a sus rutas: observarás ese comportamiento en el paso 6.

package com.ejemplo.gestor.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityBeansConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        // Factor de coste 12: equilibrio óptimo entre seguridad y tiempo de respuesta (~200ms)
        return new BCryptPasswordEncoder(12);
    }
}

Creamos una prueba unitaria para experimentar cómo opera PasswordEncoder:

package com.ejemplo.gestor;

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import static org.junit.jupiter.api.Assertions.*;

class PasswordEncoderTest {

    private final PasswordEncoder encoder = new BCryptPasswordEncoder(10);

    @Test
    void bcryptGeneraHashesDistintosParaMismaPassword() {
        String passwordPlana = "MiSuperClave2026!";

        // Generamos dos hashes de la misma contraseña exacta
        String hash1 = encoder.encode(passwordPlana);
        String hash2 = encoder.encode(passwordPlana);

        System.out.println("Hash 1: " + hash1);
        System.out.println("Hash 2: " + hash2);

        // 1. Demostración de Salt: los hashes son totalmente distintos en cada llamada
        assertNotEquals(hash1, hash2);

        // 2. Verificación de login con matches(): ambos validan con éxito
        assertTrue(encoder.matches(passwordPlana, hash1));
        assertTrue(encoder.matches(passwordPlana, hash2));

        // 3. Contraseña incorrecta rechazada
        assertFalse(encoder.matches("ClaveErronea!", hash1));
    }
}

Paso 4 · Medir el coste computacional del Work Factor

Ejecuta este benchmark en terminal para entender cómo cada incremento en el factor de coste duplica el tiempo de cálculo de la CPU:

    @Test
    void medirTiemposSegunFactorDeCoste() {
        String password = "PruebaRendimiento!";

        for (int coste = 10; coste <= 14; coste++) {
            PasswordEncoder enc = new BCryptPasswordEncoder(coste);
            long inicio = System.currentTimeMillis();
            enc.encode(password);
            long fin = System.currentTimeMillis();

            System.out.printf("Coste %d (2^%d rondas) -> Tiempo: %d ms%n", coste, coste, (fin - inicio));
        }
    }

Observa los resultados típicos en una CPU moderna:

  • Coste 10 (2¹⁰ = 1.024 iteraciones): ~60 ms.
  • Coste 11 (2¹¹ = 2.048 iteraciones): ~120 ms.
  • Coste 12 (2¹² = 4.096 iteraciones): ~240 ms. (Recomendado en servidores modernos).
  • Coste 13 (2¹³ = 8.192 iteraciones): ~490 ms.
  • Coste 14 (2¹⁴ = 16.384 iteraciones): ~980 ms.

Cada incremento duplica exactamente el coste para el atacante. El coste 12 ofrece una resistencia excepcional sin degradar la experiencia de usuario.

Paso 5 · Servicio de registro de usuario con hash seguro

Reutiliza UsuarioRepository y crea o amplía UsuarioService, recibiendo el repositorio y PasswordEncoder por constructor. El DTO público de registro contiene username y contraseña, no un rol que el visitante pueda elegir. Valida la entrada, rechaza username duplicado, codifica la contraseña y asigna desde el servidor el rol inicial menos privilegiado. Devuelve un DTO sin contraseña ni hash. Comprueba la fila guardada con una cuenta ficticia y reserva la asignación de roles elevados para una operación administrativa protegida.

  1. Inyecta PasswordEncoder en UsuarioService.
  2. Al recibir RegistroUsuarioRequest(username, password):
    • Valida que la contraseña tenga al menos 8 caracteres y complejidad mínima.
    • Codifica la contraseña antes de asignarla a la entidad: usuario.setPassword(passwordEncoder.encode(request.password()));
    • Asigna usuario.setRol(Rol.ROLE_DESARROLLADOR) desde el servidor y guarda el usuario mediante UsuarioRepository.
  3. Abre pgAdmin o tu cliente de PostgreSQL y haz un SELECT * FROM usuarios;.
  4. Comprueba visualmente que la columna password almacena una cadena que empieza por $2a$12$... y jamás la clave en texto claro.

Spring Security básico

Paso 6 · Configurar SecurityConfig con rutas públicas y privadas

La dependencia ya se añadió en el paso 3: comprueba su presencia sin duplicarla. Observa primero la respuesta sin SecurityConfig y después crea esa clase en config con una sola cadena. Conserva el bean PasswordEncoder en SecurityBeansConfig; no declares otro con el mismo nombre. Para las pruebas de esta sesión utiliza la identidad temporal indicada y verifica rutas públicas y protegidas por separado. En la sesión 37 la fuente de identidades pasará a PostgreSQL.

  1. Abre tu pom.xml y añade el starter de seguridad dentro de <dependencies>, junto a los que ya tienes:
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  2. Recarga las dependencias de Maven en tu IDE (o ejecuta ./mvnw clean compile en la terminal).
  3. Arranca la aplicación con ./mvnw spring-boot:run y no toques nada más.
  4. Busca en la terminal, entre los mensajes de arranque, una línea como esta:
    Using generated security password: 4a8b1c2d-9e3f-4123-b890-abcdef123456
    
    This generated password is for development use only. Your security configuration
    must be updated before running your application in production.
  5. Lanza GET http://localhost:8080/api/v1/proyectos sin ninguna credencial.
Qué acaba de pasar
No has escrito ni una línea de código y tu API entera ha dejado de responder con 401. Eso es Secure by Default: Spring Security prefiere que una aplicación nazca cerrada y que seas tú quien abra puertas conscientemente, antes que nacer abierta y depender de que te acuerdes de cerrarlas.
Por qué esa contraseña no sirve
Cambia en cada arranque, es la misma para todo el mundo y el usuario se llama user. Sirve para comprobar que el cerrojo está puesto y para nada más. Sustituirla es justo el trabajo del paso 3.

Crea el archivo src/main/java/com/ejemplo/gestor/config/SecurityConfig.java. Es una clase de configuración normal: no hereda de nada y no implementa ninguna interfaz; solo publica un @Bean.

package com.ejemplo.gestor.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            // 1. Desactivamos CSRF solo porque estamos construyendo una API REST sin cookies de sesión clásicas
            .csrf(csrf -> csrf.disable())

            // 2. Definimos las reglas de autorización sobre las rutas HTTP
            .authorizeHttpRequests(auth -> auth
                // Lo único público es la documentación: sin ella, quien todavía
                // no sabe cómo autenticarse no tiene por dónde empezar
                .requestMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()

                // Todo lo demás exige credenciales, tal y como declara la
                // matriz de control de acceso de la sesión 35
                .anyRequest().authenticated()
            )

            // 3. Habilitamos autenticación HTTP Basic estándar para pruebas de API
            .httpBasic(Customizer.withDefaults());

        return http.build();
    }
}
@EnableWebSecurity
Le dice a Spring que tú te haces cargo de la configuración de seguridad. En cuanto publicas tu propio SecurityFilterChain, la configuración automática se retira: ya no hay usuario user ni contraseña generada en la terminal.
http y el punto al final de cada línea
HttpSecurity aplica un patrón de construcción encadenada: cada método devuelve el mismo objeto, de modo que las llamadas se leen en cascada. El orden entre bloques (csrf, authorizeHttpRequests, httpBasic) es indiferente; el orden dentro de authorizeHttpRequests es crítico.
Por qué anyRequest() va siempre la última
Las reglas se evalúan de arriba abajo y gana la primera que encaje. Si pusieras anyRequest().authenticated() arriba, engulliría todas las peticiones y las reglas de debajo no se aplicarían nunca. Spring, de hecho, se niega a arrancar si detecta una regla inalcanzable.
/** frente a /*
/swagger-ui/* encaja con /swagger-ui/index.html pero no con /swagger-ui/css/tema.css. /** atraviesa cuantos niveles haga falta, y por eso es lo que se usa para árboles de recursos.

Con tu SecurityFilterChain publicado ya no hay contraseña generada en la terminal, así que necesitas unas credenciales propias. Para las primeras pruebas, antes de conectar la base de datos en el trabajo siguiente, valen unas estáticas en el archivo de propiedades:

# Usuario provisional para pruebas iniciales de Spring Security
spring.security.user.name=desarrollador
spring.security.user.password=Password123!
spring.security.user.roles=DESARROLLADOR

Esto es un andamio, y se cae en la sesión 37

Un usuario en un archivo de propiedades no tiene roles múltiples, no se puede dar de alta desde la API, no se puede desactivar y su contraseña viaja en texto plano dentro del repositorio. Está aquí por una única razón: para que puedas probar la cadena de filtros hoy sin arrastrar todavía la base de datos. En la sesión 37 estas tres líneas se borran.

Paso 7 · Pruebas de autorización con Bruno

Arranca tu aplicación y ejecuta estas comprobaciones desde tu cliente HTTP (Bruno o Postman):

  1. Ruta pública sin credenciales:
    • Abre en el navegador http://localhost:8080/swagger-ui.html.
    • Resultado esperado: Código 200 OK. La documentación carga sin pedir usuario, que es justo lo que necesita quien todavía no sabe cómo autenticarse.
  2. Lectura sin credenciales:
    • Lanza GET http://localhost:8080/api/v1/proyectos.
    • Resultado esperado: Código 401 Unauthorized, que es la fila ANON de la matriz de la sesión 35. Esta API no publica nada: hasta para leer hay que identificarse.
    • Revisa la pestaña Headers: el servidor ha devuelto la cabecera WWW-Authenticate: Basic realm="Realm".
  3. Escritura con credenciales válidas:
    • Abre la pestaña Auth de la petición (existe igual en Bruno y en Postman) y selecciona Basic Auth.
    • Introduce Usuario: desarrollador y Contraseña: Password123!.
    • Lanza de nuevo el POST.
    • Resultado esperado: Código 201 Created con la cabecera Location.
    • Inspecciona en Headers enviados cómo viaja: Authorization: Basic ZGVzYXJyb2xsYWRvcjpQYXNzd29yZDEyMyE= (cadena codificada en Base64).

Paso 8 · Si algo no sale como dice el guion

Los cuatro tropiezos de esta sesión, en orden de frecuencia:

Síntoma Causa casi segura Qué mirar
Sigue apareciendo Using generated security password al arrancar Sigue activa la identidad automática de desarrollo Comprueba las propiedades de usuario y, desde la sesión 37, que se detecta tu UserDetailsService dentro de donde vive GestorApplication
GET /swagger-ui.html devuelve 401 La ruta real no es la que has escrito en requestMatchers Mira en la terminal a qué ruta redirige Springdoc; suele hacer falta /swagger-ui/** y /v3/api-docs/**
Con usuario y contraseña correctos sigues recibiendo 401 El cliente no está enviando la cabecera Comprueba en la pestaña de cabeceras enviadas que aparece Authorization: Basic …; si no está, la pestaña Auth no se aplicó a esa petición
La aplicación no arranca: Cannot configure an AuthenticationProvider o una regla inalcanzable Una regla más general tapa a otra más concreta Reordena: anyRequest() siempre al final

Paso 9 · Contrastar la configuración con la matriz de permisos

Todavía no puedes distinguir un DESARROLLADOR de un ADMINISTRADOR —eso llega en la sesión 38—, pero la primera columna de la matriz ya la puedes auditar entera:

  1. Recorre la tabla de la sesión 35 y comprueba, sin credenciales, que las nueve filas de la columna ANON responden 401.
  2. Añade a SecurityConfig la única excepción que esta API sí quiere publicar: la ruta de login que construirás en la sesión 39, POST /api/v1/auth/**. Déjala escrita con permitAll() aunque el endpoint todavía no exista.
  3. Repite GET /api/v1/proyectos con las credenciales de application.properties y comprueba que ahora pasa: la ruta no ha cambiado, lo que ha cambiado es quién la pide.
  4. Guarda las tres peticiones —documentación pública, lectura anónima rechazada, lectura autenticada— como una carpeta 09-seguridad dentro de la colección que arrastras desde la UD2. A partir de aquí cada sesión de esta unidad añade peticiones a esa carpeta, y en la sesión 40 tendrás que poder ejecutarla entera de una pasada.
  5. Documenta en tu cuaderno, en tres líneas, qué ruta has abierto y por qué esa y no otra. Es la primera decisión de seguridad que tomas tú y es la que tendrás que defender.
Cómo saber que lo has terminado
Las nueve filas de la columna ANON responden 401; /swagger-ui.html responde 200 sin credenciales; la misma petición que fallaba pasa a 200 solo añadiendo Basic Auth; y la terminal de arranque ya no imprime ninguna contraseña generada.

Paso 10 · Comprobar y registrar el resultado del proyecto

  1. Comprueba con PasswordEncoder que una contraseña correcta coincide con su hash y otra no. Verifica que nunca se devuelve el hash en el DTO público.
  2. Prueba ruta pública, ruta protegida sin identidad y acceso permitido. Contrasta el resultado con la matriz y no resuelvas un rechazo abriendo todas las rutas.

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 · Migración transparente de factores de coste (upgradeEncoding)

Con el paso de los años las computadoras se vuelven más rápidas y los factores de coste antiguos (ej: coste 10 de hace 5 años) quedan desfasados.

Spring Security proporciona el método: passwordEncoder.upgradeEncoding(hashActual)

  1. ¿Cómo permite este método detectar si un hash guardado en base de datos se generó con un factor de coste inferior al estándar actual de la empresa?
  2. Diseña el flujo durante el login: si el login tiene éxito y upgradeEncoding devuelve true, ¿cómo actualiza la aplicación el hash en la base de datos con el nuevo coste sin pedirle al usuario que vuelva a escribir su contraseña?

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.

Objetivo mínimoDistinción entre cifrado reversible y hash unidireccional y bean BCryptPasswordEncoder configurado.
Si lo tienesRegistro de usuario en PostgreSQL con contraseña hasheada y test con matches() en verde.
RetoBenchmark de factores de coste ejecutado y flujo de actualización transparente (upgradeEncoding) diseñado.
Ver respuestas

1 · Porque si la clave de cifrado se ve comprometida o filtrada, todas las contraseñas de todos los usuarios de la plataforma quedan expuestas de forma inmediata.

2 · Garantiza que dos contraseñas idénticas generen hashes completamente diferentes, neutralizando los ataques basados en diccionarios y tablas arcoíris precalculadas.

3 · Porque SHA-256 fue diseñado para ser extremadamente rápido (vulnerable a fuerza bruta con GPUs), mientras que BCrypt es deliberadamente lento y configurable en coste de CPU.

4 · Porque los primeros 22 caracteres del propio hash guardado en base de datos contienen el Salt codificado, extrayéndolo de forma transparente para computar la comparación.

Reto · Manejo personalizado de respuestas 401 (RFC 7807)

Por defecto, cuando Spring Security rechaza una petición con 401, emite una respuesta vacía o el error básico de Tomcat.

Investiga la interfaz AuthenticationEntryPoint:

  1. Crea una clase CustomAuthenticationEntryPoint que implemente AuthenticationEntryPoint.
  2. Sobrescribe commence() para que ante accesos no autenticados, el servidor devuelva un JSON estructurado con el estándar RFC 7807 (Problem Details): {"status": 401, "title": "No autenticado", "detail": "Debes aportar credenciales válidas para acceder a este recurso"}.
  3. Regístralo en tu filterChain con .exceptionHandling(ex -> ex.authenticationEntryPoint(...)).
Objetivo mínimoDependencia integrada, SecurityConfig con filterChain y rutas públicas/privadas operativas.
Si lo tienesPruebas con HTTP Basic verificadas en Bruno alternando peticiones permitidas (200) y bloqueadas (401).
RetoPunto de entrada personalizado (AuthenticationEntryPoint) emitiendo respuestas RFC 7807 ante rechazos 401.
Ver respuestas

1 · Por el principio de seguridad por defecto: previene que endpoints sensibles queden expuestos accidentalmente por olvido del desarrollador.

2 · El método permitAll() encadenado a una regla de ruta con requestMatchers().

3 · Porque la cabecera Authorization solo codifica el usuario y la contraseña en Base64; Base64 no es cifrado (cualquiera puede decodificarlo al instante en texto claro).

4 · La cabecera WWW-Authenticate (ej: WWW-Authenticate: Basic realm="Realm").

Cierre

15 minutos · resultado comprobable y explicación individual

Al terminar la sesión:

No se almacenan ni devuelven contraseñas en claro y los rechazos se distinguen de los errores de negocio.

Cada integrante explica una decisión del código apoyándose en una de las comprobaciones realizadas.