Backend educativo construido con Spring Boot y Kotlin, que expone una API REST protegida con Spring Security y JWT (access + refresh), persistencia en MongoDB y validación de entrada con Bean Validation.
Este proyecto implementa un flujo de autenticación basado en tokens: al iniciar sesión se emiten un access token (corta duración) y un refresh token (larga duración). El refresh se almacena de forma hasheada en base de datos y puede rotarse al renovar la sesión. Las rutas de negocio (por ejemplo, notas) exigen un access token válido en la cabecera Authorization.
Está pensado para servir como referencia clara para reclutadores o desarrolladores que se incorporan al código: capas separadas (controladores, seguridad, persistencia), configuración mínima por variables de entorno y contratos HTTP explícitos.
| Área | Tecnología |
|---|---|
| Lenguaje | Kotlin |
| Framework | Spring Boot 4.x |
| API | Spring Web (REST) |
| Seguridad | Spring Security (sesión stateless, filtro JWT) |
| Tokens | JJWT (access / refresh con claim type) |
| Base de datos | MongoDB (Spring Data MongoDB) |
| Validación | spring-boot-starter-validation |
| Build | Gradle (Kotlin DSL) |
| JVM | Java 17 |
Se organiza en capas alineadas con una arquitectura limpia ligera:
- Presentación: controladores REST (
controllers) — DTOs de request/response y mapeo HTTP. - Aplicación / dominio de seguridad: servicios (
security) — registro, login, refresh, generación y validación de JWT. - Infraestructura: repositorios y documentos MongoDB (
database), filtro HTTP JWT, configuración de seguridad.
Las dependencias apuntan hacia adentro: los controladores delegan en servicios; la persistencia no expone detalles de Mongo a la capa HTTP.
flowchart LR
Client[Cliente HTTP] --> Controllers[Controllers]
Controllers --> AuthService[AuthService]
Controllers --> Repos[Repositories]
AuthService --> JwtService[JwtService]
AuthService --> Repos
JwtAuthFilter[JwtAuthFilter] --> JwtService
Controllers --> JwtAuthFilter
Repos --> MongoDB[(MongoDB)]
- JDK 17 (coincide con el toolchain del proyecto).
- MongoDB accesible (local, Docker o Atlas).
- Gradle (el proyecto incluye Gradle Wrapper; no es obligatorio tener Gradle instalado globalmente).
git clone <URL_DE_TU_REPO>
cd spring_boot_crash_courseCrea un archivo .env, exporta variables en tu shell o configura tu IDE con al menos:
MONGODB_CONNECTION_STRINGJWT_SECRET_BASE64
(Los nombres coinciden con application.properties.)
Ejemplo local típico:
mongodb://localhost:27017/YOUR_DB_NAME
Para MongoDB Atlas, usa la URI que proporciona el panel (usuario, contraseña y cluster).
El código decodifica jwt.secret desde Base64 y lo usa como clave HMAC-SHA256. Debes usar una cadena Base64 que, al decodificar, tenga suficiente entropía (por ejemplo 32 bytes aleatorios codificados en Base64).
Ejemplo (solo para desarrollo; en producción usa un gestor de secretos):
openssl rand -base64 32Asigna el resultado a JWT_SECRET_BASE64.
| Variable | Descripción |
|---|---|
MONGODB_CONNECTION_STRING |
URI de conexión a MongoDB (incluye base de datos si aplica). |
JWT_SECRET_BASE64 |
Secreto HMAC en Base64 (coherente con Keys.hmacShaKeyFor / HS256). |
Opcionalmente puedes añadir en application.properties (o por entorno) otras propiedades estándar de Spring, por ejemplo server.port, si necesitas un puerto distinto del 8080 por defecto.
Desde la raíz del repositorio:
./gradlew bootRunCon variables en la misma línea (macOS / Linux):
export MONGODB_CONNECTION_STRING="mongodb://localhost:27017/YOUR_DB_NAME"
export JWT_SECRET_BASE64="$(openssl rand -base64 32)"
./gradlew bootRunLa API quedará disponible en http://localhost:8080 (salvo que cambies server.port).
Ejecutar tests:
./gradlew testBase URL de ejemplo: http://localhost:8080.
POST /auth/register — público
Request
{
"email": "usuario@ejemplo.com",
"password": "MiClaveSegura1"
}Reglas de contraseña (validación): mínimo 9 caracteres, al menos una minúscula, una mayúscula y un dígito.
Response — cuerpo vacío con éxito (HTTP 200). Si el email ya existe, la aplicación puede responder con error según el manejo de excepciones configurado.
POST /auth/login — público
Request
{
"email": "usuario@ejemplo.com",
"password": "MiClaveSegura1"
}Response (ejemplo)
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}Credenciales incorrectas → 401 Unauthorized (según Spring Security / BadCredentialsException).
POST /auth/refresh — público (el cuerpo lleva el refresh, no la cabecera Bearer del access)
Request
{
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}Response (ejemplo) — nuevo par de tokens (rotación del refresh almacenado):
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiJ9..."
}Refresh inválido, expirado o ya usado → 401 Unauthorized.
GET /notes — requiere autenticación
Headers
Authorization: Bearer <accessToken>Response (ejemplo)
[
{
"id": "674a1b2c3d4e5f6789012345",
"title": "Compras",
"content": "Leche, pan",
"color": 4294901760,
"createdAt": "2026-03-31T12:00:00Z"
}
]POST /notes — requiere autenticación
Headers
Authorization: Bearer <accessToken>
Content-Type: application/jsonRequest (nueva nota — sin id o con id opcional según tu flujo)
{
"id": null,
"title": "Ideas",
"content": "API REST con JWT",
"color": 4278190080
}Response (ejemplo)
{
"id": "674a1b2c3d4e5f6789012346",
"title": "Ideas",
"content": "API REST con JWT",
"color": 4278190080,
"createdAt": "2026-03-31T12:05:00Z"
}DELETE /notes/{id} — requiere autenticación (solo si la nota pertenece al usuario autenticado)
Headers
Authorization: Bearer <accessToken>Response — sin cuerpo con éxito (según implementación del controlador).
| Concepto | Comportamiento en este proyecto |
|---|---|
| Access token | Se envía en Authorization: Bearer <token>. Duración aproximada 15 minutos. Incluye claim type: "access". El filtro JwtAuthFilter valida el token y establece el principal como el ID de usuario (hex de ObjectId) en el contexto de seguridad. |
| Refresh token | Duración larga (30 días en código). Claim type: "refresh". No va en rutas /notes para autorización; se usa solo contra POST /auth/refresh. En servidor se guarda un hash SHA-256 (Base64) del refresh en MongoDB; al usar el refresh se invalida el anterior y se emite uno nuevo (rotación). |
| Rutas públicas | Todo bajo /auth/** está permitido sin Bearer. El resto exige un access token válido. |
Flujo recomendado para clientes: guardar ambos tokens tras el login; cuando el access expire, llamar a /auth/refresh con el refresh; actualizar almacenamiento con el nuevo par.
spring_boot_crash_course/
├── build.gradle.kts
├── settings.gradle.kts
├── gradle/
├── src/
│ ├── main/
│ │ ├── kotlin/com/jesushzc/spring_boot_crash_course/
│ │ │ ├── SpringBootCrashCourseApplication.kt
│ │ │ ├── GlobalValidationHandler.kt
│ │ │ ├── controllers/
│ │ │ │ ├── AuthController.kt
│ │ │ │ └── NoteController.kt
│ │ │ ├── security/
│ │ │ │ ├── SecurityConfig.kt
│ │ │ │ ├── JwtAuthFilter.kt
│ │ │ │ ├── JwtService.kt
│ │ │ │ ├── AuthService.kt
│ │ │ │ └── HashEncoder.kt
│ │ │ └── database/
│ │ │ ├── model/ # User, Note, RefreshToken
│ │ │ └── repository/ # Spring Data repositories
│ │ └── resources/
│ │ └── application.properties
│ └── test/
│ └── kotlin/...
└── README.md
- API stateless con Spring Security y sin sesión en servidor para la API REST.
- Separación por capas (controladores, servicios de seguridad, persistencia).
- Contraseñas nunca en claro en base de datos (hash con componente dedicado).
- Refresh token rotativo y almacenado de forma hasheada (no se guarda el JWT en bruto).
- Validación declarativa en DTOs de entrada (
@Valid,@Email,@Pattern,@NotBlank). - JWT con tipo explícito (
access/refresh) para reducir confusión entre tokens. - Configuración externa sensible vía variables de entorno (
MONGODB_CONNECTION_STRING,JWT_SECRET_BASE64).
- Documentación OpenAPI (SpringDoc / Swagger UI) generada desde los controladores.
- Refresh token con detección de reutilización (logout global / revocación en cadena).
- Rate limiting y política CORS explícita si hay front en otro origen.
- Tests de integración para flujos
/authy/notescon@SpringBootTesty contenedor Testcontainers para MongoDB. - Refresh en cookie HttpOnly frente a solo cuerpo JSON, valorando riesgos XSS/CSRF.
- Homogeneizar respuestas de error (códigos y cuerpos JSON) en un
@ControllerAdvicecentralizado.
Nombre / contacto: Jesus Clemente Hernandez / jclementeh07@gmail.com