# Día 16: Seguridad Stateless con Spring Security 6 y JWT en Java 17/21

> SecurityFilterChain, Sesiones sin Estado, Filtros OncePerRequestFilter y Autorización por Método

- Plan: [JavaMastery](https://aprende-java.webs-economicas.es/), día 16 de 30
- Fase 4: Seguridad, Concurrencia y Reactividad vs Virtual Threads
- Dedicación: 4h
- Profundidad: para dominar
- Revisado: 2026-10-29
- Versión interactiva: https://aprende-java.webs-economicas.es/dias/spring-security-jwt/

## Al terminar este día sabrás

- Configurar Spring Security 6 con `SecurityFilterChain` y el DSL de lambdas.
- Entender qué contiene un JWT, qué garantiza y qué no.
- Seguir el recorrido de una petición por la cadena de filtros hasta el `SecurityContext`.
- Autorizar por método y almacenar contraseñas con un algoritmo adaptativo.


## Conceptos

### 1. De WebSecurityConfigurerAdapter a SecurityFilterChain con Lambdas

En Spring Security 6 ya no se extiende de clases base obsoletas. Se expone un `@Bean public SecurityFilterChain filterChain(HttpSecurity http)` donde cada regla (autorización, csrf, sessionManagement) se define con lambdas fluidas sin encadenamientos rotos con `.and()`.

**Cómo funciona por dentro.**

Spring Security 6 eliminó `WebSecurityConfigurerAdapter`. La configuración se declara ahora como un bean `SecurityFilterChain` construido con `HttpSecurity`, y el DSL usa lambdas en lugar de encadenar `.and()`. El resultado es más legible y permite tener varias cadenas, por ejemplo una para `/api/**` sin estado y otra para una consola web con sesión, ordenadas con `@Order`.

Las reglas de `authorizeHttpRequests` se evalúan en orden y gana la primera que coincide, así que las más específicas van primero y se termina con `anyRequest()`. Un error frecuente es poner `anyRequest().authenticated()` al principio y que nunca se evalúen las demás.

Para una API REST sin estado, lo típico es desactivar la sesión (`STATELESS`), desactivar CSRF (si el token viaja en una cabecera, no en una cookie) y configurar el servidor de recursos OAuth2 para validar JWT.

*Cadena de seguridad para una API sin estado*

```java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
class SeguridadConfig {

    @Bean
    SecurityFilterChain api(HttpSecurity http) throws Exception {
        return http
            .securityMatcher("/api/**")
            .csrf(csrf -> csrf.disable())                         // token en cabecera, no en cookie
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers(HttpMethod.POST, "/api/auth/login").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers(HttpMethod.GET, "/api/tareas/**").hasAuthority("SCOPE_tareas:leer")
                .anyRequest().authenticated())                    // siempre al final
            .oauth2ResourceServer(o -> o.jwt(Customizer.withDefaults()))
            .exceptionHandling(e -> e
                .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())   // 401
                .accessDeniedHandler(new BearerTokenAccessDeniedHandler()))            // 403
            .build();
    }
}
```

- **En una línea:** `http.csrf(csrf -> csrf.disable()).authorizeHttpRequests(auth -> auth.requestMatchers("/api/auth/**").permitAll().anyRequest().authenticated());`
- **Por qué importa:** Facilita la modularidad y elimina métodos obsoletos propensos a fallos de configuración.
- **Error típico:** Copiar código antiguo de StackOverflow que aún intenta usar `extends WebSecurityConfigurerAdapter` o el método `.and()`.
- **Java 17 frente a 21:** Obligatorio desde Spring Boot 3.0 en Java 17 y 21.
- **Se conecta con:** Spring Bean Container, DSL Fluida, Filtros HTTP

### 2. Arquitectura de Autenticación Stateless con Tokens JWT

En una API REST escalable el servidor no guarda sesiones en memoria (`SessionCreationPolicy.STATELESS`). Cada petición HTTP entrante incluye un token JWT firmado criptográficamente en la cabecera `Authorization: Bearer <token>`.

**Cómo funciona por dentro.**

Un JWT son tres partes en Base64URL separadas por puntos: cabecera (algoritmo), carga útil (claims como `sub`, `exp`, `iat`, `scope` o datos propios) y firma. La firma garantiza integridad y autenticidad: si alguien cambia un solo carácter de la carga, la firma deja de cuadrar. Pero no cifra nada: cualquiera puede decodificar la carga y leerla, así que nunca pongas datos sensibles dentro.

La ventaja de un token sin estado es que el servidor no guarda sesiones: valida la firma y la expiración y confía en su contenido, lo que escala horizontalmente sin compartir memoria. La contrapartida es que no se puede revocar fácilmente antes de que expire. Por eso los access tokens duran poco (5-15 minutos) y se acompañan de refresh tokens más largos que sí se guardan y se pueden invalidar.

Con HS256 la misma clave secreta firma y verifica; con RS256 o ES256 se firma con una clave privada y se verifica con la pública, lo que permite que muchos servicios validen sin poder emitir tokens.

*Validar JWT con el servidor de recursos de Spring*

```yaml
# Opción 1: un proveedor de identidad (Keycloak, Auth0, Entra ID) publica sus claves
spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://auth.acme.com/realms/taskflow
          # Spring descarga las claves públicas (JWKS) y valida firma, exp, nbf e iss

# Opción 2: tokens emitidos por la propia aplicación con clave pública RSA
#   jwt:
#     public-key-location: classpath:keys/public.pem

# Un token decodificado (la firma se omite):
# cabecera: {"alg":"RS256","typ":"JWT","kid":"2026-03"}
# carga:    {"sub":"ana","scope":"tareas:leer tareas:escribir",
#            "iat":1772359200,"exp":1772360100,"iss":"https://auth.acme.com/realms/taskflow"}
```

- **En una línea:** `http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));`
- **Por qué importa:** Permite escalar horizontalmente la aplicación en múltiples réplicas (Docker, Kubernetes) sin necesidad de replicar sesiones HTTP en memoria.
- **Error típico:** Almacenar contraseñas, sueldos o datos personales sensibles dentro del payload del JWT creyendo que el token está cifrado (solo está firmado y codificado en Base64).
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Microservicios, Criptografía HMAC / RSA, Escalabilidad Horizontal

### 3. El Filtro OncePerRequestFilter y el SecurityContextHolder

Un filtro interceptor que se ejecuta exactamente una vez por cada petición HTTP. Extrae el JWT, valida su firma con la clave secreta, extrae el usuario y sus roles, y crea un `UsernamePasswordAuthenticationToken` guardándolo en `SecurityContextHolder.getContext().setAuthentication(auth)`.

**Cómo funciona por dentro.**

Spring Security es, en esencia, una cadena de filtros de Servlet que se ejecuta antes de llegar a tu controlador. Cada filtro tiene una responsabilidad: leer credenciales, autenticar, gestionar excepciones, autorizar. `OncePerRequestFilter` es la clase base para escribir uno propio con la garantía de que se ejecuta una sola vez por petición, aunque haya reenvíos internos.

Cuando un filtro autentica al usuario, guarda un objeto `Authentication` en el `SecurityContext`, que vive en un `SecurityContextHolder` asociado al hilo actual (un `ThreadLocal`). A partir de ahí, la autorización por URL, `@PreAuthorize` y tus controladores (con `@AuthenticationPrincipal`) leen de ese contexto. Al terminar la petición, el contexto se limpia.

Para JWT no hace falta escribir el filtro: `oauth2ResourceServer` ya registra `BearerTokenAuthenticationFilter`. Un filtro propio tiene sentido para esquemas no estándar, como una API key de servicio a servicio.

*Filtro propio para claves de API internas*

```java
class ApiKeyFilter extends OncePerRequestFilter {
    private final Map<String, String> clavesPorServicio;   // en un caso real, hash de las claves

    ApiKeyFilter(Map<String, String> clavesPorServicio) { this.clavesPorServicio = clavesPorServicio; }

    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws ServletException, IOException {
        String clave = req.getHeader("X-Api-Key");
        String servicio = clave == null ? null : clavesPorServicio.get(clave);

        if (servicio != null) {
            var auth = new UsernamePasswordAuthenticationToken(
                servicio, null, List.of(new SimpleGrantedAuthority("ROLE_SERVICIO")));
            SecurityContext ctx = SecurityContextHolder.createEmptyContext();
            ctx.setAuthentication(auth);
            SecurityContextHolder.setContext(ctx);         // visible para el resto de la petición
        }
        chain.doFilter(req, res);                          // sin clave: sigue anónimo y decidirá la autorización
    }
}

// Registro en la cadena:
// http.addFilterBefore(new ApiKeyFilter(claves), BearerTokenAuthenticationFilter.class);
```

- **En una línea:** `SecurityContextHolder.getContext().setAuthentication(authToken); filterChain.doFilter(request, response);`
- **Por qué importa:** Hace que todo el resto de la aplicación (servicios, repositorios, controladores) sepa quién es el usuario autenticado llamando a `SecurityContextHolder`.
- **Error típico:** Olvidar llamar a `filterChain.doFilter(request, response)` al final del filtro, congelando la petición del cliente para siempre.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Filter Chain de Servlet, ThreadLocal, Auditoría JPA

### 4. Autorización Declarativa a Nivel de Método con @PreAuthorize

Anotación activada mediante `@EnableMethodSecurity` que permite restringir la ejecución de métodos según roles (`hasRole("ADMIN")`) o permisos individuales usando SpEL (Spring Expression Language).

**Cómo funciona por dentro.**

Las reglas por URL son gruesas: dicen quién puede llamar a `/api/tareas/**`, pero no si Ana puede editar la tarea 42. Para eso está la seguridad a nivel de método, que se activa con `@EnableMethodSecurity` (en Spring Security 6 sustituye a `@EnableGlobalMethodSecurity`).

`@PreAuthorize` evalúa una expresión SpEL antes de ejecutar el método. Puede consultar roles (`hasRole`), autoridades (`hasAuthority`), el usuario autenticado (`authentication.name`), los parámetros del método (`#id`) y llamar a otros beans (`@permisos.puedeEditar(...)`). `@PostAuthorize` evalúa después y puede usar el valor devuelto (`returnObject`).

Mover la lógica de permisos a un bean dedicado mantiene las expresiones cortas y permite probarla con tests unitarios normales. Si la expresión falla, se lanza `AccessDeniedException`, que se traduce en un `403`.

*Autorización por propietario con un bean de permisos*

```java
@Service
class TareaService {

    @PreAuthorize("hasRole('ADMIN') or @permisos.esResponsable(#id, authentication.name)")
    public TareaResponse editar(long id, EditarTareaRequest req) { /* ... */ return null; }

    @PreAuthorize("hasAuthority('SCOPE_tareas:leer')")
    @PostAuthorize("returnObject.publica() or returnObject.responsable() == authentication.name")
    public TareaResponse obtener(long id) { /* ... */ return null; }
}

@Component("permisos")
class Permisos {
    private final TareaRepository repo;
    Permisos(TareaRepository repo) { this.repo = repo; }

    public boolean esResponsable(long tareaId, String usuario) {
        return repo.findResponsable(tareaId).map(usuario::equals).orElse(false);
    }
}
```

- **En una línea:** `@PreAuthorize("hasRole("ADMIN")") public void eliminarUsuario(Long id) { ... }`
- **Por qué importa:** Aplica el principio de defensa en profundidad: la seguridad no se limita a las URLs, sino que protege la propia capa de servicios del dominio.
- **Error típico:** Usar la anotación antigua y deprecada `@EnableGlobalMethodSecurity`.
- **Java 17 frente a 21:** Idéntico en 17 y 21.
- **Se conecta con:** Spring AOP, RBAC (Role-Based Access Control), Defensa en Profundidad

### 5. Cifrado Fuerte de Contraseñas con BCryptPasswordEncoder

Las contraseñas jamás se almacenan en texto plano ni con algoritmos rápidos como MD5 o SHA-256. Se utiliza `BCryptPasswordEncoder`, que introduce un factor de coste (work factor) y una sal (salt) aleatoria en cada hash.

**Cómo funciona por dentro.**

Las contraseñas nunca se cifran (eso permitiría descifrarlas) ni se guardan con un hash rápido como SHA-256: un atacante con una GPU puede probar miles de millones de hashes por segundo. Se usan funciones de hash diseñadas para ser lentas y con sal: BCrypt, Argon2 o PBKDF2.

BCrypt genera una sal aleatoria por contraseña (así dos usuarios con la misma contraseña tienen hashes distintos y las tablas precalculadas no sirven) y aplica un factor de coste: con coste 10 hace 2^10 rondas; cada punto más duplica el tiempo. La idea es que una verificación tarde del orden de 100 ms: imperceptible para un usuario e inviable para un ataque masivo. El resultado guarda algoritmo, coste, sal y hash en una sola cadena.

Spring recomienda `DelegatingPasswordEncoder`, que antepone el algoritmo (`{bcrypt}$2a$10$...`). Así puedes cambiar a Argon2 en el futuro y seguir verificando las contraseñas antiguas, actualizándolas cuando el usuario inicie sesión.

*Codificar y verificar contraseñas*

```java
@Bean
PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();   // {bcrypt} por defecto
}

@Service
class RegistroService {
    private final PasswordEncoder encoder;
    private final UsuarioRepository repo;

    RegistroService(PasswordEncoder encoder, UsuarioRepository repo) {
        this.encoder = encoder;
        this.repo = repo;
    }

    public void registrar(String email, String contrasena) {
        String hash = encoder.encode(contrasena);
        // {bcrypt}$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
        //          |  |  |-- 22 caracteres de sal + 31 de hash
        //          |  coste 10 (2^10 rondas)
        //          versión del algoritmo
        repo.save(new Usuario(email, hash));
    }

    public boolean credencialesValidas(String email, String contrasena) {
        return repo.findByEmail(email)
            .map(u -> encoder.matches(contrasena, u.hash()))   // nunca compares hashes con equals
            .orElse(false);
    }
}
```

- **En una línea:** `String hash = passwordEncoder.encode(passwordPlana); boolean ok = passwordEncoder.matches(intento, hash);`
- **Por qué importa:** Invalida por completo los ataques con tablas arcoíris (rainbow tables) y mitiga ataques de fuerza bruta si la base de datos es filtrada.
- **Error típico:** Comparar contraseñas cifradas con `equals()` en lugar de utilizar `passwordEncoder.matches(passwordPlana, passwordCifrada)`.
- **Java 17 frente a 21:** Estándar en Java 17 y 21.
- **Se conecta con:** Criptografía, Hashing, Seguridad OWASP


## Sesión de 90 minutos

### Bloque 1: SecurityFilterChain en Spring Boot 3 (0 - 25 min)

- **Qué estudiar:** Configuración lambda con `SecurityFilterChain` y sesión stateless.
- **Pregunta a responder:** ¿Cómo permitir rutas públicas (`/api/auth/**`) y proteger el resto?
- **Entregable:** Clase `SecurityConfig` funcional con rutas públicas y privadas.

### Bloque 2: Generación y Validación de JWT (25 - 50 min)

- **Qué estudiar:** Librería `jjwt` o `nimbus-jose-jwt`. Firma HMAC-SHA256.
- **Pregunta a responder:** ¿Cómo verificar la expiración y claims de un token?
- **Entregable:** Servicio `JwtService` con tests unitarios verificando firma y expiración.

### Bloque 3: Filtro de Autenticación HTTP (50 - 80 min)

- **Qué estudiar:** Clase `OncePerRequestFilter` y población del `SecurityContextHolder`.
- **Pregunta a responder:** ¿Cómo extraer el token de la cabecera `Authorization: Bearer`?
- **Entregable:** Filtro que autentica al usuario antes de alcanzar los controladores.

### Cierre y Consolidación (80 - 90 min)

- **Qué estudiar:** Prueba de endpoints con `@PreAuthorize("hasRole("ADMIN")")`.
- **Pregunta a responder:** Verificar que un usuario sin rol recibe HTTP 403 Forbidden.
- **Entregable:** Progreso registrado en la app.


## Explícalo desde cero

### Por qué no se debe usar SHA-256 simple para almacenar contraseñas

- **Qué entender:** SHA-256 está diseñado para ser extremadamente rápido (procesar gigabytes por segundo para verificar integridad de ficheros). Un atacante con una GPU moderna puede calcular miles de millones de hashes por segundo. BCrypt está diseñado intencionalmente para ser lento y costoso en CPU y memoria, haciendo inviable la fuerza bruta.
- **Qué suele explicarse mal:** Confundir un algoritmo de hash criptográfico general (SHA-256) con una función de derivación de claves para contraseñas con sal y coste ajustable (BCrypt, Argon2).
- **Ejemplo sencillo:** Una cerradura normal que se abre en milisegundos vs una cerradura de caja fuerte que impone un retardo de 500 ms por cada intento de llave, bloqueando a quien pretenda probar un millón de llaves seguidas.
- **Señal de alerta:** Si propones hacer `MessageDigest.getInstance("SHA-256")` para contraseñas en una entrevista técnica.


## Preguntas

### Nivel 1 (Básico): ¿Por qué en Spring Security 6 se suele desactivar CSRF (`csrf.disable()`) en APIs REST que utilizan tokens JWT?

Evalúa si entiendes el vector de ataque de Cross-Site Request Forgery. Los ataques CSRF dependen de que el navegador envíe automáticamente cookies de sesión en peticiones cruzadas. Las APIs REST que autentican mediante el header `Authorization: Bearer <token>` no usan cookies y por ende son inmunes a CSRF.

**Respuesta razonada:** CSRF explota que el navegador adjunta automáticamente las cookies de un dominio en cualquier petición a ese dominio, aunque la inicie otra web. Si la sesión viaja en una cookie, una página maliciosa puede hacer que tu navegador envíe un `POST` autenticado sin que te enteres.

Si la API se autentica con un JWT en la cabecera `Authorization: Bearer ...`, el navegador no la añade solo: la tiene que poner el código JavaScript de tu aplicación, y una web de otro origen no puede leer tu token ni añadir esa cabecera. Sin credenciales automáticas, no hay ataque CSRF posible y la protección sobra. Ojo: si guardas el JWT en una cookie, vuelves a necesitar protección CSRF (o cookies `SameSite=Strict`).

### Nivel 2 (Intermedio): ¿Cuál es la diferencia exacta entre un Role y una Authority en Spring Security?

Una Authority representa un permiso individual y granular (e.g. `OP_CREAR_FACTURA`). Un Role es una agrupación de permisos que por convención en Spring Security se prefija con `ROLE_` (e.g. `hasRole("ADMIN")` busca internamente la autoridad `ROLE_ADMIN`).

**Respuesta razonada:** Para Spring Security ambos son `GrantedAuthority`, es decir, cadenas de texto. Un rol es simplemente una autoridad con el prefijo `ROLE_`. `hasRole('ADMIN')` comprueba si existe la autoridad `ROLE_ADMIN`; `hasAuthority('ADMIN')` busca exactamente `ADMIN`, sin prefijo. Mezclarlos es el error más común: guardar `ADMIN` y comprobar `hasRole('ADMIN')` siempre deniega.

Conceptualmente, un rol agrupa permisos de grano grueso (ADMIN, EDITOR) y una autoridad suele representar un permiso concreto (`tareas:borrar`). Con el servidor de recursos JWT, los scopes del token se convierten en autoridades con prefijo `SCOPE_`, así que se comprueban con `hasAuthority('SCOPE_tareas:leer')`.

### Nivel 3 (Entrevista Senior): ¿Qué problema ocurre con `SecurityContextHolder` cuando lanzas tareas asíncronas con `@Async` o Virtual Threads si no configuras la propagación del contexto?

Por defecto, `SecurityContextHolder` almacena la autenticación en un `ThreadLocal` exclusivo del hilo que atiende la petición HTTP. Si la ejecución salta a otro hilo o a un pool de fondo, el nuevo hilo no hereda el contexto y las llamadas a `@PreAuthorize` fallarán por considerarlo anónimo. Se resuelve configurando `MODE_INHERITABLETHREADLOCAL` o usando un `DelegatingSecurityContextExecutor`.

**Respuesta razonada:** Por defecto, `SecurityContextHolder` guarda el contexto en un `ThreadLocal`. Si lanzas trabajo en otro hilo con `@Async`, un `ExecutorService` o un hilo virtual nuevo, ese hilo no tiene contexto: `getAuthentication()` devuelve `null` y la autorización falla o, peor, el código se comporta como anónimo.

La solución es propagar el contexto explícitamente: envolver el ejecutor con `DelegatingSecurityContextExecutorService` (o `DelegatingSecurityContextAsyncTaskExecutor` para `@Async`), que copia el contexto al lanzar la tarea y lo limpia al terminar. Spring Boot puede hacerlo con un `TaskDecorator`, y la librería Micrometer Context Propagation unifica esto para seguridad, trazas y MDC. La estrategia `MODE_INHERITABLETHREADLOCAL` no es buena idea con pools, porque los hilos reutilizados heredan el contexto de quien los creó, no el de la tarea.


## Reto del tutor

**Reto:** Un desarrollador propone: "Voy a meter toda la información del usuario (sueldo, dirección, teléfono, permisos) dentro del payload del token JWT para no tener que consultar la base de datos nunca más". Evalúa esta propuesta como un arquitecto senior.

- **Lo que está bien:** Reconoce que el JWT transporta claims para evitar consultas repetitivas de identidad.
- **Respuesta superficial:** Decir únicamente que "el token va a pesar mucho".
- **Lo que falta de fondo:** No advertir que el payload del JWT NO está cifrado (solo está firmado y en Base64url), exponiendo datos confidenciales y personales a cualquiera que intercepte la cabecera HTTP.
- **Respuesta senior:** Es una brecha grave de seguridad y un fallo de diseño: (1) El JWT está firmado (integridad garantizada), pero NO encriptado: cualquiera puede decodificar el Base64url y leer datos personales sensibles (violación de GDPR/privacidad); (2) Aumentar el tamaño del token obliga a transmitir esos kilobytes extra en CADA petición HTTP de la red; (3) Inconsistencia de estado: si el rol o estado del usuario cambia en la base de datos, el token emitido seguirá siendo válido con datos obsoletos hasta su expiración. En el JWT solo debe viajar la identidad mínima (`sub: idUsuario`) y los roles necesarios para la autorización.


## Katas

### Kata 16.1: SecurityFilterChain Stateless con Endpoints Públicos y Protegidos

Crea una clase `@Configuration` con un bean `SecurityFilterChain` en Spring Boot 3 que permita acceso anónimo a `/api/v1/auth/**` y `/actuator/health`, pero exija autenticación para cualquier otra ruta con sesión `STATELESS`.

### Kata 16.2: Filtro de Validación JWT con OncePerRequestFilter

Implementa un `JwtAuthenticationFilter` que extraiga el header `Authorization`, valide el token con una clave HMAC secreta y pueble el `SecurityContextHolder` con los roles del usuario.


## Reglas de oro

- Jamás guardes secretos criptográficos quemados (hardcoded) en el código; inyéctalos desde variables de entorno con `@Value("${jwt.secret}")`.
- El payload de un JWT es público para quien tenga el token; nunca guardes datos sensibles o privados allí.
- En APIs REST stateless desactiva CSRF (`csrf.disable()`) y configura `SessionCreationPolicy.STATELESS`.
- Asegúrate de que tu `OncePerRequestFilter` siempre llame a `filterChain.doFilter()` en el bloque finally o flujo normal.
- Utiliza siempre `BCryptPasswordEncoder` o `Argon2` para validar contraseñas con `passwordEncoder.matches()`.


## Java 17 frente a Java 21

| Característica | Java 17 | Java 21 |
| --- | --- | --- |
| Soporte Virtual Threads | No disponible en 17 | Compatible mediante DelegatingSecurityContextExecutor |


## Proyecto guiado «Pedidos»: Seguridad con JWT y roles

Que cada cliente vea solo sus pedidos y que solo la tienda pueda cambiar su estado.

**Tareas:**

- Configura una `SecurityFilterChain` stateless que valide tokens JWT.
- Define los roles `CLIENTE` y `ADMIN`: el cliente crea y consulta sus pedidos; el admin confirma, envía y cancela.
- Usa `@PreAuthorize` para que un cliente no pueda leer los pedidos de otro.

**Está listo cuando:**

- Pedir el pedido de otro cliente devuelve 403, y hacerlo sin token, 401.
- Las contraseñas se guardan con BCrypt.


## Fuentes oficiales

- [Spring Security: arquitectura](https://docs.spring.io/spring-security/reference/servlet/architecture.html)
- [Spring Security: servidor de recursos con JWT](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html)
- [RFC 7519: JSON Web Token](https://www.rfc-editor.org/rfc/rfc7519)

