08 - Login con JWT
La tienda ya vende con transacciones; le falta saber quién compra y quién administra. Este hito aplica el tema 06 — Login con JWT: el módulo users con registro y login, el par de tokens — access JWT de vida corta y refresh opaco persistido — y la validación stateless en un filtro propio. La autorización declarativa llega en el hito siguiente.
| Pieza | Cambio en este hito |
|---|---|
| Contrato | v1.0.0: registro y login; el pedido toma la identidad del token |
| Estructura | Módulo users nuevo, que nace ya con JPA |
| Mecanismo | Access JWT + refresh opaco con rotación; filtro Servlet propio |
1. El contrato evoluciona
- Ampliar
openapi.yamla v1.0.0 — cambio breaking, por eso salta de versión mayor:
| Operación | Petición | Respuestas |
|---|---|---|
| Registro | POST /api/users · UserRequest | 201 · 400 · 409 |
| Login | POST /api/auth/login · LoginRequest | 200 · 401 |
| Refresh | POST /api/auth/refresh · RefreshToken | 200 · 401 |
| Logout | POST /api/auth/logout · RefreshToken | 204 · 401 |
| Mis pedidos | GET /api/me/orders | 200 · 401 |
- Registro:
name,emailypassword(mínimo 8). Login y refresh devuelvenTokenResponse:accessToken,refreshTokenyexpiresIn. El logout revoca el refresh que recibe. - El cambio breaking:
OrderRequestpierdeuserId— la identidad del comprador ya no viaja en el cuerpo, sino en el token. Los pedidos de prueba de los hitos anteriores se limpian: suuserIdera provisional.
2. El módulo users
- Módulo nuevo en la verificación, con la estructura aprendida en el hito 07: interfaces de repositorio en
domainy mecanismo JPA enpersistence. - Tablas
usersyrefresh_tokensde la especificación: ampliar elinit.sqly reiniciar el volumen de MariaDB. La semilla trae un usuarioADMINcon su hash; el registro crea siempreUSER. - Contraseñas hash, nunca en claro. El dominio no conoce el mecanismo de hashing: define lo que necesita y la composición aporta la implementación, de la forma que decida cada quien; en el hito 09 pasará a la maquinaria de Spring Security sin tocar el dominio.
- El login devuelve el par de tokens: access JWT de vida corta, firmado en el servidor, y refresh opaco — una cadena aleatoria sin significado, persistida por su hash con su caducidad y su capacidad de revocación. El refresh se usa una vez: al usarlo se revoca y se entrega uno nuevo (rotación); el logout revoca el refresh recibido. Los claims del access token bastan para que el resto de la aplicación reconozca la identidad — id y rol — sin conocer nada del módulo users.
El access token es stateless, pero el par no: el refresh rota al usarse y se revoca en el logout. La tabla
refresh_tokens de la especificación existe por eso.
3. Autenticación en el filtro
- Aún sin Spring Security, la protección es un filtro Servlet propio, como en el tema 06: extrae el token de la cabecera, valida firma y caducidad, deja la identidad a la vista de los controladores y responde 401 en las rutas que la exigen. Los filtros no pasan por los manejadores de la presentación: el 401 lo escribe el filtro con el formato del contrato.
- Las rutas protegidas son todavía una lista gestionada a mano en el filtro.
- Con la identidad en marcha,
GET /api/me/ordersdevuelve solo los pedidos del comprador: la identidad viene del token, no de un parámetro.
La lista de rutas «a mano» está a propósito: muestra el límite del enfoque. En el hito 09 la sustituye la cadena declarativa de Spring Security, sin cambiar el flujo de tokens.
4. Pruebas
- Registro: usuario correcto → 201; email repetido → 409; contraseña corta → 400.
- Login: credenciales correctas → 200 con el par de tokens; usuario o contraseña incorrectos → 401.
- Refresh: correcto → par nuevo y el viejo revocado; reusar el viejo → 401; refresh caducado → 401.
- Logout: revoca el refresh; usarlo después → 401.
- Filtro: ruta protegida sin token → 401; con token caducado o mal firmado → 401; rutas públicas sin token pasan; la identidad llega al controlador con id y rol.
- Mis pedidos: devuelve solo los del comprador del token.
Comprobación
- El flujo completo corre de punta a punta: registro → login → pedido → mis pedidos → cancelación → refresh → logout.
- El contrato está en v1.0.0 y el Swagger refleja las operaciones nuevas;
OrderRequestya no llevauserId. - La verificación de límites sigue verde con el tercer módulo.
- Commit y tag
hito-08.
Siguiente hito: hito 09 — Integración con Spring Security.