Fundamentos de Arquitectura Java: Módulo de Autenticación para Plataformas de E-Commerce

Estrategia de Modularización con Maven

El punto de partida consiste en estructurar el proyecto mediante un modelo padre-hijo. El artefacto principle actúa como gestor de dependencias, mientras que los módulos secundarios heredan su configuración. La cadena de herencia se define de manera lógica: la capa de API depende de la capa de servicios, esta última consume la capa de mapeo, la cual referencia los modelos de datos, y finalmente estos dependen de una biblioteca base compartida.

Herramientas de Modelado de Datos

Para estandarizar el diseño del esquema relacional, se emplean herramientas especializadas como PDMan. Estas utilidades permiten visualizar diagramas entidad-relación, generar scripts DDL automáticos y mantener un registro histórico de versiones sobre las estructuras tabulares.

Integración del Motor Spring Boot

1. Declaración de Dependencias

La coordinación entre el motor de aplicaciones, el compilador y el sistema de bases de datos es crítica. Se recomienda validar la compatibilidad cruzada antes de consolidar el archivo de construcción.

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.2.0</version>
    <relativePath/>
</parent>

<properties>
    <maven.compiler.source>17</maven.compiler.source>
    <maven.compiler.target>17</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <java.version>17</java.version>
</properties>

<dependencies>
    <!-- Núcleo web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Procesador de configuración opcional -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-configuration-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- Persistencia con MyBatis-Plus v3.5.5+ -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.5</version>
        <exclusions>
            <exclusion>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis-spring</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <!-- Controlador JDBC MySQL 8+ -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
        <version>8.2.0</version>
    </dependency>

    <!-- Simplificador de accesores -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>1.18.30</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

2. Archivo de Propiedades

Se definen los parámetros de escucha, el conjunto de conexiones HikariCP y el comportamiento del maper.

server:
  port: 8090
  tomcat:
    uri-encoding: UTF-8
    max-http-header-size: 80KB

spring:
  datasource:
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/tienda_online_dev?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC&autoReconnect=true
    username: admin_db
    password: clave_segura
    hikari:
      connection-timeout: 30000
      minimum-idle: 5
      maximum-pool-size: 20
      auto-commit: true
      idle-timeout: 600000
      pool-name: PoolConexionesHikari
      max-lifetime: 1800000
      connection-test-query: SELECT 1

mybatis:
  type-aliases-package: com.ecommerce.auth.modelo
  mapper-locations: classpath:mappers/*.xml

mybatis-plus:
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      id-type: auto
      logic-delete-field: eliminado
      logic-delete-value: 1
      logic-not-delete-value: 0

MyBatis-Plus extiende las capacidades nativas sin alterar el núcleo, por lo que representa una elección sólida. Las capas de entidades y DAO pueden generarse automáticamente mediante plantillas o asistentes inteligentes que interpreten la estructura del DTO y la tabla destino.

3. Clase Principle

package com.ecommerce.auth.configuracion;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@ComponentScan(basePackages = {"com.ecommerce.auth", "com.lib.generadores.id"})
public class AplicacionPrincipal {
    public static void main(String[] args) {
        SpringApplication.run(AplicacionPrincipal.class, args);
    }
}

Definición de Contratos REST

Las rutas siguen convenciones semánticas HTTP. Para facilitar la integración con clientes front-end, se implementa documentación interactiva migrada hacia estándares OpenAPI 3, utilizando Knife4j para mejorar la experiencia visual.

Dependencias Adicionales

<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.3.0</version>
</dependency>

<dependency>
    <groupId>com.github.xiaoymin</groupId>
    <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
    <version>4.4.0</version>
</dependency>

Configuración Descriptiva

package com.ecommerce.auth.configuracion;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DocumentacionAPI {

    @Bean
    public OpenAPI configurarEndpoints() {
        return new OpenAPI()
                .info(new Info()
                        .title("Gestión de Usuarios API")
                        .version("2.1.0")
                        .description("Contratos para operaciones de autenticación y registro")
                        .contact(new Contact()
                                .name("Soporte Técnico")
                                .email("soporte@dominio.dev")));
    }
}

Inclusión en application.yml:

springdoc:
  api-docs:
    enabled: true
    path: /v3/api-docs
  swagger-ui:
    enabled: true
    path: /swagger-ui.html
  group-configs:
    - group: 'default'
      paths-to-match: '/**'
      packages-to-scan: com.ecommerce.auth.controlador

knife4j:
  enable: true
  setting:
    language: zh_cn
    swagger-model-name: Lista de Entidades

Resolución de Restricciones Cruzadas (CORS)

Se habilita la comunicación entre dominios configurando un filtro dedicado:

package com.ecommerce.auth.configuracion;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;

@Configuration
public class SeguridadCruceDominios {

    @Bean
    public CorsFilter aplicadorCors() {
        CorsConfiguration configuracion = new CorsConfiguration();
        configuracion.addAllowedOrigin("http://localhost:3000");
        configuracion.setAllowCredentials(true);
        configuracion.addAllowedMethod("*");
        configuracion.addAllowedHeader("*");

        UrlBasedCorsConfigurationSource fuente = new UrlBasedCorsConfigurationSource();
        fuente.registerCorsConfiguration("/api/**", configuracion);

        return new CorsFilter(fuente);
    }
}

Implementación de Funcionalidades Críticas

Verificación de Disponibilidad de Identificador

Antes de proceder al alta, se valida si el nombre seleccionado ya está registrado.

@Service
public class ServicioUsuariosImpl extends ServiceImpl<usuariodao usuario=""> implements ServicioUsuarios {

    private final UsuarioDao daoUsuario;

    public ServicioUsuariosImpl(UsuarioDao daoUsuario) {
        this.daoUsuario = daoUsuario;
    }

    @Override
    public boolean verificarDisponibilidadIdentificador(String identificador) {
        if (identificador == null || identificador.trim().isEmpty()) {
            return false;
        }
        var consulta = new LambdaQueryWrapper<Usuario>();
        consulta.eq(Usuario::getNombreUsuario, identificador);
        return daoUsuario.exists(consulta);
    }
}</usuariodao>

Gestión de Alta de Cuenta

Se utiliza un Objeto de Transferencia (DTO) para encapsular solo los campos necesarios, evitando exponer atributos internos como la confirmación repetida de contraseña.

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SolicitudRegistro {
    @NotBlank(message = "El usuario es requerido")
    private String nombreUsuario;
    
    @Size(min = 6, message = "La clave debe tener al menos 6 caracteres")
    private String claveSecreta;
    
    private String confirmarClave;
}

Lógica de persistencia con transformación hash y generación distribuida de identificadores:

@Service
public class GestionAlta {
    
    private static final String AVATAR_POR_DEFECTO = "https://cdn.ejemplo.com/avatars/default.png";

    public Usuario registrar(SolicitudRegistro solicitud) {
        if (!solicitud.getClaveSecreta().equals(solicitud.getConfirmarClave())) {
            throw new IllegalArgumentException("Las claves no coinciden");
        }

        Usuario perfil = new Usuario();
        perfil.setId(GeneradorIDDistrito.obtenerTokenCorto());
        perfil.setNombreUsuario(solicitud.getNombreUsuario());
        perfil.setClave(HashUtil.encriptarMD5(solicitud.getClaveSecreta()));
        perfil.setApodo(solicitud.getNombreUsuario());
        perfil.setRostro(AVATAR_POR_DEFECTO);
        perfil.fechaCreacion(new Date());
        perfil.estadoActivo(true);

        daoUsuario.insert(perfil);
        return perfil;
    }
}

Controlador de Operaciones

Respuesta estandarizada y enmascaramiento de datos sensibles antes de serializar hacia Cookies.

@RestController
@RequestMapping("/api/auth")
@Tag(name = "Autenticación y Acceso")
public class ControladorAcceso {

    private final ServicioUsuarios servicio;
    private final GestionAlta alto;
    private final CookieHelper manejadorCookies;

    @PostMapping("/verificar-usuario")
    public RespuestaOperacion<boolean> comprobarExistencia(@RequestParam String usuario) {
        boolean disponible = !servicio.verificarDisponibilidadIdentificador(usuario);
        return RespuestaOperacion.exito(disponible ? "Identificador libre" : "Ya registrado", disponible);
    }

    @PostMapping("/crear-cuenta")
    public RespuestaOperacion<object> darDeAlta(@RequestBody SolicitudRegistro payload,
                                                HttpServletRequest req,
                                                HttpServletResponse res) {
        alto.registrar(payload);
        Usuario perfilRecuperado = servicio.consultarPorNombre(payload.getNombreUsuario());
        
        // Ocultar información sensible
        PerfilDTO dtoSeguro = ocultarDatosCriticos(perfilRecuperado);
        manejadorCookies.guardarEnCliente(req, res, "token_sesion", Serializador.objetoATexto(dtoSeguro));
        
        return RespuestaOperacion.exito("Cuenta creada exitosamente");
    }

    private PerfilDTO ocultarDatosCriticos(Usuario u) {
        return new PerfilDTO(u.getId(), u.getNombreUsuario(), u.getApodo(), null);
    }
}</object></boolean>

Contenedor genérico de retorno:

@Data
public class RespuestaOperacion<T> {
    private Integer codigo;
    private String mensaje;
    private T contenido;
    private Long fechaHoraIso;

    public RespuestaOperacion(Integer codigo, String mensaje, T contenido) {
        this.codigo = codigo;
        this.mensaje = mensaje;
        this.contenido = contenido;
        this.fechaHoraIso = System.currentTimeMillis();
    }

    public static <T> RespuestaOperacion<T> exito(String msg, T data) {
        return new RespuestaOperacion<>(200, msg, data);
    }
    
    public static <T> RespuestaOperacion<T> fallo(String msg) {
        return new RespuestaOperacion<>(500, msg, null);
    }
}

Ingreso al Sistema

Validación de credenciales contra el repositorio cifrado y establecimiento de contexto:

@PostMapping("/entrar")
public RespuestaOperacion<object> iniciarSesion(@RequestBody CredencialesLogin credenciales,
                                                HttpServletRequest req,
                                                HttpServletResponse res) {
    if (StringUtils.isEmpty(credenciales.getUsuario()) || StringUtils.isEmpty(credenciales.getClave())) {
        return RespuestaOperacion.fallo("Propetario vacío");
    }

    Usuario valido = servicio.buscarCredencial(credenciales.getUsuario(), HashUtil.encriptarMD5(credenciales.getClave()));
    if (valido == null) {
        return RespuestaOperacion.fallo("Combinación inválida");
    }

    PerfilDTO datosPublicos = ocultarDatosCriticos(valido);
    manejadorCookies.guardarEnCliente(req, res, "sesion_activa", Serializador.objetoATexto(datosPublicos));
    
    return RespuestaOperacion.exito("Acceso concedido", datosPublicos);
}</object>

Terminación de Sesión

@PostMapping("/salir")
public RespuestaOperacion<void> finalizarSesion(@RequestParam String idUsuario,
                                                HttpServletRequest req,
                                                HttpServletResponse res) {
    manejadorCookies.eliminarElemento(req, res, "sesion_activa");
    // Pendiente: limpieza de carrito persistente y invalidación en cache distribuido
    return RespuestaOperacion.exito("Sesión cerrada", null);
}</void>

Métricas de Ejecución vía Aspect Oriented Programming

Para auditar el rendimiento de las capas de negocio, se aplica un corte transversal que mide el ciclo de vida de los métodos anotados.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
package com.ecommerce.auth.interceptores;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class MonitorDeRendimiento {

    private static final Logger traza = LoggerFactory.getLogger(MonitorDeRendimiento.class);

    @Around("execution(* com.ecommerce.auth.servicio.impl..*.*(..))")
    public Object medirTiempoProcesamiento(ProceedingJoinPoint joinPoint) throws Throwable {
        long inicio = System.currentTimeMillis();
        String descripcion = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();
        
        traza.info("[INICIO OPERACIÓN] {}", descripcion);
        
        Object resultado = joinPoint.proceed();
        
        long duracionMs = System.currentTimeMillis() - inicio;
        traza.info("[FIN OPERACIÓN] {} | Tiempo transcurrido: {}ms", descripcion, duracionMs);
        
        if (duracionMs > 3000) {
            traza.error("[ADVERTENCIA CRÍTICA] Operación lenta: {}", duracionMs);
        } else if (duracionMs > 1500) {
            traza.warn("[AVISO] Latencia elevada detectada: {}", duracionMs);
        }
        
        return resultado;
    }
}

Etiquetas: spring-boot-3 mybatis-plus-3.5 openapi-jakarta cors-filter-java aspect-oriented-programming

Publicado el 7-28 14:38