Saltar a contenido

Backend

El backend es un servicio Spring Boot que concentra todo el dominio del Sistema de Premios: valida vouchers, administra el padrón de clientes, corre el motor de sorteo, genera el acta reproducible y expone la API que consumen la landing y el gestor. Es, además, quien empaqueta y sirve esos dos frontends — no hay servidor web separado.

Esta página es la referencia del backend como componente: stack, estructura de paquetes, arranque, seguridad y build. Para el detalle de configuración operativa (variables de entorno, perfiles), ver Configuración.

Stack

Tecnología Versión Para qué
Java 17 (maven.compiler.release) Lenguaje base
Spring Boot 4.1.1 Framework de aplicación (última GA al momento del upgrade, ver ADR-018)
Tomcat 11 (embebido, gestionado por Boot 4) Servidor HTTP
Hibernate 7 (gestionado por Boot 4) ORM, ddl-auto: validate
Jackson 3 (paquete tools.jackson.*) Serialización JSON, SNAKE_CASE
SQL Server 2022 Base de datos, driver mssql-jdbc
Flyway módulo flyway-sqlserver Migraciones versionadas, corren solas al boot
Maven Build, mvn package

Jackson 3 no es com.fasterxml.jackson.*

Spring Boot 4 trae Jackson 3, que renombra jackson-core/jackson-databind al paquete tools.jackson.*. Las anotaciones (com.fasterxml.jackson.annotation.*) no cambiaron. JsonProcessingException (checked) desaparece a favor de JacksonException (unchecked). Si copiás un snippet viejo con import com.fasterxml.jackson.databind.ObjectMapper, no compila — el import correcto es tools.jackson.databind.ObjectMapper. Detalle completo en ADR-018.

Estructura de paquetes

Todo vive bajo com.tipre.sistemapremios:

backend/src/main/java/com/tipre/sistemapremios/
├── SistemaPremiosApplication.java   # entrypoint @SpringBootApplication
├── adjudicacion/                    # asignación de premios a ganadores/suplentes
├── api/                             # controllers públicos + filtros HTTP
│   ├── dto/                         # DTOs de la API pública (snake_case)
│   └── gestor/                      # controllers del gestor (autenticados) + sus DTOs
├── clavefirma/                      # cifrado/descifrado del secreto HMAC (ClaveFirma)
├── config/                          # configuración de Flyway por perfil (e2e, load)
├── dominio/                         # entidades JPA (Campania, Cliente, Sorteo, Chance, ...)
├── modoprueba/                      # generador de vouchers de prueba (solo gestor, nunca prod)
├── participacion/                   # servicio central: resolver/confirmar/alta, reconocimiento
├── premio/                          # PremioUnidad y su ciclo de estado
├── qrtoken/                         # espejo Java del token firmado (compartido con el POS)
├── repositorio/                     # Spring Data JPA repositories
├── soporte/                         # arranque: config externa, seed, avisos de env vars legacy
└── sorteo/                          # motor de sorteo + generación de acta

api/ concentra toda la superficie HTTP. Lo que distingue a api/gestor/ del resto no es la ubicación en disco sino el prefijo de ruta /api/gestor/**, que es lo que GestorSecurityConfig usa para exigir autenticación.

qrtoken es un módulo vendoreado, no reimplementado

El backend no reinventa la validación del token del QR: usa el mismo módulo que corre en el POS (espejo Java de un módulo C89). Ver El token del QR para el formato del payload y el flujo de firma/validación.

Cómo arranca

El entrypoint es mínimo — toda la lógica de arranque vive en soporte/ y en la configuración de Spring Boot:

@SpringBootApplication
public class SistemaPremiosApplication {
    public static void main(String[] args) {
        SpringApplication.run(SistemaPremiosApplication.class, args);
    }
}

Secuencia real de arranque:

  1. Chequeo de config externa (ConfigExternaEnvironmentPostProcessor, corre antes que cualquier bean) — exige que ./config/application.yml exista y traiga el centinela sp.config.externa: true, más las claves de contrato (SNAKE_CASE, ddl-auto: validate). Sin eso, el proceso ni intenta levantar el contexto. Ver Configuración → config externa.
  2. Flyway migra la base automáticamente. No hay paso manual: al conectar, Flyway aplica cualquier migración pendiente contra SistemaPremios antes de que el contexto termine de levantar.
  3. Hibernate valida el esquema contra las entidades (ddl-auto: validate) — nunca lo altera. Si el esquema real no coincide con lo que las entidades esperan, el arranque falla ahí, no en producción silenciosamente.
  4. Beans de seguridad y guardas de arranque fail-fastGestorSecurityConfig exige SP_GESTOR_PASSWORD, ClaveFirmaConfig exige SP_MASTER_KEY, ModoPruebaConfig corta el arranque si sp.modo-prueba.enabled=true y el perfil activo contiene prod.
  5. SeedMinimo (opcional) — solo si se pasa --sp.seed.archivo=<ruta.json>, carga campaña + sorteos + clave de firma desde un JSON, idempotente. Es el fallback operativo si el gestor no llega a tiempo para cargar la campaña a mano.

Seguridad

Spring Security cubre solo lo que necesita quedar detrás de credenciales; el resto de la API pública queda permitAll a propósito (participación, padrón, bases, campaña vigente, salud, branding — la landing es anónima por diseño).

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/participacion/**", "/api/padron/**", "/bases/**",
            "/api/campania/vigente", "/api/salud", "/api/branding", "/branding/**")
    .permitAll()
    .requestMatchers("/actuator/health").permitAll()
    .requestMatchers("/actuator/**").authenticated()
    .requestMatchers("/api/gestor/**").authenticated()
    .anyRequest().permitAll())
.httpBasic(Customizer.withDefaults())
  • /api/gestor/** exige HTTP Basic, sin sesión (SessionCreationPolicy.STATELESS) — el gestor manda el header Authorization en cada request, nunca una cookie.
  • Usuario y password del gestor vienen de SP_GESTOR_USER/SP_GESTOR_PASSWORD, nunca hardcodeados. Si falta SP_GESTOR_PASSWORD, el arranque falla con IllegalStateException — a propósito. La versión anterior generaba una password aleatoria y la logueaba en texto claro para que el operador la leyera; eso viola la regla de que un secreto nunca aparece en logs, así que se sacó: sin credencial, no arranca.
  • /actuator/** queda cerrado salvo /actuator/health, y ese health no muestra detalle para un caller anónimo (show-details: when-authorized). actuator llega transitivo con la consola de desarrollo bootui (ver ADR-018) — sin esta restricción explícita, /actuator/env, configprops, beans, loggers, mappings y conditions quedaban alcanzables sin autenticación, volcando variables de entorno completas.
  • CSP estricta, con dos excepciones angostas y documentadas: 'wasm-unsafe-eval' en script-src (necesario para compilar el WebAssembly del escáner de DNI, no habilita eval() de JS arbitrario) y camera=(self) en Permissions-Policy (la landing usa la cámara para escanear).

Rate limiting

Filtro propio, sin dependencias nuevas — mitigación mínima contra abuso (fuerza bruta de tokens, scraping), no un sustituto de un WAF real.

Config Default Qué hace
sp.rate-limit.enabled true apagado en el perfil de test para no disparar 429 espurios contra bursts legítimos de la suite
sp.rate-limit.limite 30 requests permitidos por ventana
sp.rate-limit.ventana-ms 60000 tamaño de la ventana (60 s)
sp.rate-limit.cap-entradas 10000 tope de IPs trackeadas antes de purgar entradas vencidas

Cubre /api/** y también /p/** (el estado inicial de la landing se resuelve server-side ahí, así que un scan de tokens contra /p/{token} pega tan fuerte como uno directo a POST /api/participacion/resolver).

El filtro se registra explícito, ANTES de Security

RateLimitFilterConfig lo registra con Ordered.HIGHEST_PRECEDENCE, a propósito. Si quedara con el orden por defecto de Spring Boot, correría después de la cadena de seguridad: un intento con credenciales inválidas contra /api/gestor/** nunca llegaría a contarse para el rate limit, y un atacante podría probar passwords sin freno.

Detrás de un reverse proxy, X-Forwarded-For tiene que estar sanitizado

Sin proxy, request.getRemoteAddr() ya es la IP real. Con un proxy delante (Caddy en el runbook de despliegue), el proxy debe pisar el X-Forwarded-For entrante con la IP real del cliente — si no, cualquiera puede falsificar su IP en el header y esquivar el rate limit. forward-headers-strategy: framework ya está activado en application.yml para que Spring resuelva getRemoteAddr() correctamente detrás del proxy.

Modo prueba

Generador de vouchers de prueba (con su propio QR) invocado desde el gestor, para poder probar el flujo completo sin imprimir tickets reales. Vive detrás de dos guardas independientes:

  1. sp.modo-prueba.enabled (default false): si la feature no está prendida, no existe (404), no solo está "escondida".
  2. Fail-fast contra prod: si sp.modo-prueba.enabled=true y el perfil activo contiene prod, el contexto no levanta. No hay combinación posible de "modo prueba en producción".

Seed inicial (SeedMinimo)

Fallback para cargar la campaña sin depender de que el gestor esté listo. Se activa solo con --sp.seed.archivo=<ruta.json> (nunca por default) y es idempotente: correrlo dos veces sobre el mismo archivo no duplica campaña, sorteos ni clave de firma.

java -jar sistemapremios-backend.jar --sp.seed.archivo=seed-minimo.json

El archivo trae campania, sorteos[] y clave_firma (con el secreto en texto plano, nunca committeado — ver docs/seed-minimo.ejemplo.json en el repo del proyecto para el formato). Antes de escribir nada valida que campania.key_id_vigente coincida con clave_firma.key_id: un typo ahí dejaría la campaña firmando con una clave que no es la declarada, y se corta antes de tocar la base.

Cifrado de secretos at-rest

El secreto HMAC que firma los tokens del QR (clave_firma.secreto_cifrado) nunca se persiste en claro. CifradorSecretos cifra con AES-256-GCM, usando una master key de 32 bytes que llega por SP_MASTER_KEY (Base64) y nunca se guarda en el repo.

  • Formato: IV(12 bytes) || ciphertext+tag(GCM) — IV aleatorio en cada cifrado, nunca reusado.
  • Sin SP_MASTER_KEY configurada, el bean de CifradorSecretos ni se registra; en su lugar se registra un bean que falla con un mensaje explícito (SP_MASTER_KEY no configurada) en vez del genérico NoSuchBeanDefinitionException que daría Spring por defecto.
  • Un secreto_cifrado que no autentica (master key incorrecta o dato alterado) lanza IllegalStateException al descifrar — nunca decodifica basura en silencio.

Build del jar único

El perfil Maven full (activo por defecto — se desactiva con -DskipFrontends=true) bundlea la landing y el gestor dentro del jar del backend, vía frontend-maven-plugin:

cd backend
mvn package -Pfull

Diagrama

Las ejecuciones de npm y el copy de los dist/ están atadas a la fase prepare-package (después de test), así que mvn test del día a día nunca paga el costo de construir los dos frontends. Node se descarga localmente (v20.18.1, vía frontend-maven-plugin), no hace falta tenerlo instalado en la máquina de build.

El resultado es un único artefacto: backend/target/sistemapremios-backend-0.0.1-SNAPSHOT.jar, que sirve la API, la landing (/, assets en /assets/**) y el gestor (/gestor/, assets en /gestor/assets/**) — ver Instalar en el servidor para el despliegue real.

QA y análisis estático

Perfil Maven qa (atado a verify, nunca a test, para no encarecer el ciclo diario): checkstyle + PMD + SpotBugs (con findsecbugs). Ninguno de los tres rompe el build por sí mismo (failOnViolation/failOnError en false) — el gate real lo aplica el pipeline de calidad del proyecto sobre los reportes XML que generan.

jacoco-maven-plugin (0.8.15) instrumenta la suite en cada mvn test y emite el reporte XML de cobertura.

Dependencias notables

Dependencia Para qué
spring-boot-starter-web API REST
spring-boot-starter-data-jpa Persistencia (Hibernate 7)
spring-boot-starter-validation Bean Validation en DTOs de entrada
spring-boot-starter-security HTTP Basic para /api/gestor/**
mssql-jdbc Driver SQL Server (runtime)
bootui-spring-boot-starter (1.14.1) Consola de desarrollo (introspección salud/JPA/seguridad); solo activa en dev/local, rechaza requests no-loopback — ver ADR-018
spring-boot-starter-flyway + flyway-sqlserver Migraciones versionadas
com.google.zxing:core / javase (3.5.3) Genera el PNG del QR de los vouchers de prueba (solo ModoPruebaService, nunca en el camino de producción real)
spring-boot-starter-webmvc-test (test) Soporte de MockMvc (módulo separado desde Boot 4)