- Agile 2
- Alta disponibilidad 1
- Alternativas cloud 1
- Aop 1
- Arquitectura 3
- Arquitectura distribuida 2
- Automatizacion 3
- Azure devops 1
- Base de datos 1
- Buenas practicas 19
- Cloud 1
- Colas 7
- Competing consumers 1
- Convenciones 11
- Copilot 1
- Diseno 6
- Docker 2
- Docker compose 1
- Documentacion 1
- Eda 11
- Equipos 1
- Escalabilidad 1
- Flujo de negocio 1
- Flujo de trabajo 3
- Flyway 1
- Git 4
- Gradle 3
- Herramientas digitales 1
- Ia 1
- Iam 1
- Infraestructura 2
- Java 14
- Jerarquia tecnica 1
- Jpa 1
- Jsonb 1
- Kafka 7
- Kubernetes 1
- Liderazgo en software 1
- Lineamientos 1
- Log 1
- Logging 3
- Microservicios 3
- Mongodb 1
- Monitoreo 1
- Nosql 3
- Observabilidad 4
- Open source 1
- Plugins 3
- Postgresql 1
- Privacidad 1
- Programacion funcional 1
- Programacion reactiva 4
- Rabbitmq 6
- Rotacion de talento 1
- Saga 2
- Scrum 2
- Security 1
- Seguridad 1
- Self hosting 1
- Sistemas legados 1
- Spring boot 3
- Spring mvc 2
- Sql 3
- Streams 1
- Threadlocal 1
- Trazabilidad 2
- Versionado 2
- Web 1
- Webflux 2
- Websockets 1
- Zero trust 1
Spring boot
3 artículos
Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security
- Mauricio ECR
- Snippets
- 03 Aug, 2025
En el desarrollo de aplicaciones empresariales modernas, la seguridad es un pilar fundamental. Sin embargo, lograr una arquitectura de seguridad que sea reutilizable, desacoplada y, al mismo t
Una Propuesta para Estandarizar la Seguridad en APIs REST con Arquitectura Hexagonal y Spring Security
- Mauricio ECR
- Snippets
- 03 Aug, 2025
En el desarrollo de aplicaciones empresariales modernas, la seguridad es un pilar fundamental. Sin embargo, lograr una arquitectura de seguridad que sea reutilizable, desacoplada y, al mismo tiempo, compatible con los estándares de la industria (como JWT y Spring Security) puede ser un reto. Este artículo explora una solución basada en la arquitectura hexagonal, que permite centralizar la lógica de autorización en el dominio, sin perder la integración con las capacidades avanzadas de Spring Security, como el uso de anotaciones (@PreAuthorize) y la inyección del usuario autenticado en los controladores.
Contexto y Desafío
La mayoría de los frameworks modernos, como Spring Boot, ofrecen mecanismos de seguridad robustos y listos para usar. Sin embargo, estos suelen acoplar la lógica de autenticación y autorización a la infraestructura, dificultando la reutilización y el testeo independiente del dominio. Por otro lado, la arquitectura hexagonal promueve la separación de responsabilidades, permitiendo que la lógica de negocio (incluida la autorización) permanezca independiente de los detalles tecnológicos.
El desafío surge cuando se requiere que la lógica de autorización, implementada en el dominio, pueda interactuar con el ecosistema de Spring Security, permitiendo el uso de anotaciones como @PreAuthorize y la inyección del usuario autenticado en los controladores. La solución propuesta en este artículo aborda este reto, permitiendo una integración fluida y flexible.
Dependencias Requeridas
Para implementar esta solución, se requieren las siguientes dependencias en el archivo build.gradle:
dependencies {
// Spring Boot Core
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-configuration-processor'
// Lombok para reducir boilerplate
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
// JWT (opcional, para parsing de tokens)
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
// Testing
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
Estructura de Directorios
La librería sigue una estructura de directorios que respeta los principios de la arquitectura hexagonal:
src/
├── main/
│ ├── java/
│ │ └── com/
│ │ └── tuempresa/
│ │ ├── dominio/
│ │ │ └── autorizacion/
│ │ │ ├── AuthorizationContext.java
│ │ │ ├── AuthorizationException.java
│ │ │ ├── AuthorizationService.java
│ │ │ └── AuthorizationServiceImpl.java
│ │ └── infraestructura/
│ │ ├── config/
│ │ │ ├── AuthorizationConfig.java
│ │ │ └── AuthorizationFilterConfig.java
│ │ └── filtros/
│ │ └── StandardAuthorizationFilter.java
│ └── resources/
│ └── META-INF/
│ └── spring.factories (para auto-configuración)
└── test/
└── java/
└── com/
└── tuempresa/
├── dominio/
│ └── autorizacion/
│ └── AuthorizationServiceTest.java
└── infraestructura/
└── filtros/
└── StandardAuthorizationFilterTest.java
Solución: Librería de Seguridad Hexagonal Integrada
La solución se basa en una serie de clases y componentes que pueden ser empaquetados como una librería reutilizable. Esta librería permite:
- Centralizar la lógica de autorización en el dominio, desacoplada de la infraestructura.
- Configurar rutas excluidas, parámetros y comportamiento desde archivos de configuración.
- Integrar con Spring Security para habilitar anotaciones y acceso al usuario autenticado.
- Validar JWT y extraer roles/claims para el contexto de seguridad.
A continuación, se presentan las clases clave de la solución.
1. Contexto de Autorización (Dominio)
package com.tuempresa.dominio.autorizacion;
import lombok.Builder;
import lombok.Value;
import java.util.Map;
@Value
@Builder
public class AuthorizationContext {
String method;
String uri;
Map<String, String> headers;
Map<String, String> queryParams;
String body;
String remoteAddress;
}
2. Excepción de Dominio
package com.tuempresa.dominio.autorizacion;
public class AuthorizationException extends RuntimeException {
public AuthorizationException(String message) {
super(message);
}
}
3. Puerto de Dominio (Interface)
package com.tuempresa.dominio.autorizacion;
public interface AuthorizationService {
void authorize(AuthorizationContext context) throws AuthorizationException;
}
4. Implementación Base del Servicio de Dominio
package com.tuempresa.dominio.autorizacion;
public class AuthorizationServiceImpl implements AuthorizationService {
@Override
public void authorize(AuthorizationContext context) {
String token = context.getHeaders().get("authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new AuthorizationException("Token inválido o ausente");
}
// Más lógica de dominio...
}
}
5. Configuración del Filtro
package com.tuempresa.infraestructura.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.HashSet;
@Data
@Component
@ConfigurationProperties(prefix = "app.security.authorization")
public class AuthorizationFilterConfig {
private Set<String> excludedPaths = new HashSet<>();
private boolean enabled = true;
private int order = 1;
private boolean includeQueryParams = true;
private boolean includeBody = false;
private boolean enableSpringSecurityIntegration = true;
public AuthorizationFilterConfig() {
excludedPaths.add("/actuator/health");
excludedPaths.add("/actuator/info");
excludedPaths.add("/swagger-ui");
excludedPaths.add("/v3/api-docs");
excludedPaths.add("/error");
}
}
6. Filtro Principal Consolidado
package com.tuempresa.infraestructura.filtros;
import com.tuempresa.dominio.autorizacion.AuthorizationContext;
import com.tuempresa.dominio.autorizacion.AuthorizationService;
import com.tuempresa.dominio.autorizacion.AuthorizationException;
import com.tuempresa.infraestructura.config.AuthorizationFilterConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.core.annotation.Order;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.util.ContentCachingRequestWrapper;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.*;
@Component
@Order(1)
@ConditionalOnProperty(name = "app.security.authorization.enabled", havingValue = "true", matchIfMissing = true)
@RequiredArgsConstructor
public class StandardAuthorizationFilter extends OncePerRequestFilter {
private final AuthorizationFilterConfig config;
private final AuthorizationService authorizationService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
if (isExcludedPath(request.getRequestURI())) {
filterChain.doFilter(request, response);
return;
}
try {
HttpServletRequest requestToUse = request;
if (config.isIncludeBody()) {
requestToUse = new ContentCachingRequestWrapper(request);
}
Map<String, String> headers = extractHeaders(requestToUse);
Map<String, String> queryParams = config.isIncludeQueryParams() ? extractQueryParams(requestToUse) : Collections.emptyMap();
String requestBody = (config.isIncludeBody() && requestToUse instanceof ContentCachingRequestWrapper)
? extractRequestBody((ContentCachingRequestWrapper) requestToUse)
: null;
AuthorizationContext context = AuthorizationContext.builder()
.method(requestToUse.getMethod())
.uri(requestToUse.getRequestURI())
.headers(headers)
.queryParams(queryParams)
.body(requestBody)
.remoteAddress(requestToUse.getRemoteAddr())
.build();
authorizationService.authorize(context);
// Integración con Spring Security (si está habilitada)
if (config.isEnableSpringSecurityIntegration()) {
setSpringSecurityContext(context);
}
filterChain.doFilter(requestToUse, response);
} catch (AuthorizationException e) {
handleSecurityException(response, e);
} catch (Exception e) {
logger.error("Error in authorization filter", e);
handleGenericError(response);
}
}
private Map<String, String> extractHeaders(HttpServletRequest request) {
Map<String, String> headers = new HashMap<>();
Enumeration<String> headerNames = request.getHeaderNames();
while (headerNames.hasMoreElements()) {
String headerName = headerNames.nextElement();
String headerValue = request.getHeader(headerName);
headers.put(headerName.toLowerCase(), headerValue);
}
return headers;
}
private Map<String, String> extractQueryParams(HttpServletRequest request) {
Map<String, String> queryParams = new HashMap<>();
Enumeration<String> paramNames = request.getParameterNames();
while (paramNames.hasMoreElements()) {
String paramName = paramNames.nextElement();
String paramValue = request.getParameter(paramName);
queryParams.put(paramName, paramValue);
}
return queryParams;
}
private String extractRequestBody(ContentCachingRequestWrapper request) throws IOException {
byte[] content = request.getContentAsByteArray();
if (content.length > 0) {
return new String(content, StandardCharsets.UTF_8);
}
return null;
}
private void setSpringSecurityContext(AuthorizationContext context) {
try {
String authHeader = context.getHeaders().get("authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String username = extractUsernameFromToken(authHeader);
List<String> roles = extractRolesFromToken(authHeader);
List<SimpleGrantedAuthority> authorities = roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
.toList();
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.warn("Could not set Spring Security context", e);
}
}
private String extractUsernameFromToken(String authHeader) {
// Implementar parsing del JWT aquí
return "user_from_jwt"; // Placeholder
}
private List<String> extractRolesFromToken(String authHeader) {
// Implementar parsing del JWT aquí
return List.of("USER"); // Placeholder
}
private boolean isExcludedPath(String requestURI) {
return config.getExcludedPaths().stream()
.anyMatch(excluded -> requestURI.startsWith(excluded));
}
private void handleSecurityException(HttpServletResponse response, AuthorizationException e) throws IOException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write(String.format(
"{\"error\":\"Unauthorized\",\"message\":\"%s\",\"timestamp\":\"%s\"}",
e.getMessage(), new Date()
));
}
private void handleGenericError(HttpServletResponse response) throws IOException {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
response.setContentType("application/json");
response.getWriter().write(String.format(
"{\"error\":\"Internal Server Error\",\"message\":\"Authorization check failed\",\"timestamp\":\"%s\"}",
new Date()
));
}
}
7. Configuración de Beans
package com.tuempresa.infraestructura.config;
import com.tuempresa.dominio.autorizacion.AuthorizationService;
import com.tuempresa.dominio.autorizacion.AuthorizationServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AuthorizationConfig {
@Bean
public AuthorizationService authorizationService() {
return new AuthorizationServiceImpl();
}
}
8. Configuración en application.yml
app:
security:
authorization:
enabled: true
order: 1
include-query-params: true
include-body: false
enable-spring-security-integration: true
excluded-paths:
- "/actuator/health"
- "/actuator/info"
- "/swagger-ui"
- "/v3/api-docs"
- "/public"
- "/auth/login"
9. Ejemplo de Uso en el Proyecto Cliente
@Service
public class MyCustomAuthorizationService implements AuthorizationService {
@Override
public void authorize(AuthorizationContext context) throws AuthorizationException {
String token = context.getHeaders().get("authorization");
if (!isValidJWT(token)) {
throw new AuthorizationException("JWT inválido");
}
if (context.getUri().startsWith("/admin") && !hasAdminRole(token)) {
throw new AuthorizationException("Acceso denegado a área administrativa");
}
}
private boolean isValidJWT(String token) {
// Implementar validación JWT
return true;
}
private boolean hasAdminRole(String token) {
// Verificar roles en el JWT
return false;
}
}
10. Uso en Controllers
@RestController
public class MyController {
@GetMapping("/protected")
@PreAuthorize("hasRole('USER')")
public String protectedEndpoint(@AuthenticationPrincipal String username) {
return "Hello " + username + "! You are authenticated.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminEndpoint() {
return "Admin area";
}
}
Consideraciones Finales
- Separación de responsabilidades: El dominio se mantiene puro y desacoplado de la infraestructura.
- Integración total: Se habilita el uso de anotaciones y la inyección del usuario autenticado gracias a la integración con el contexto de Spring Security.
- Configurabilidad: La solución es fácilmente adaptable a distintos proyectos mediante configuración externa.
- Reutilización: El diseño modular permite empaquetar la solución como una librería para múltiples aplicaciones.
Conclusión
La estandarización de la seguridad bajo una arquitectura hexagonal, combinada con la integración de Spring Security y JWT, permite construir aplicaciones robustas, mantenibles y alineadas con las mejores prácticas de la industria. Esta aproximación no solo facilita la reutilización y el testeo, sino que también habilita la evolución futura del sistema, permitiendo incorporar nuevas estrategias de autenticación o autorización sin comprometer la arquitectura.
Gestión de Migraciones de Base de Datos con Flyway en Spring Boot"
- Mauricio ECR
- Persistencia
- 14 Apr, 2025
Introducción El desarrollo de aplicaciones modernas no solo implica escribir código de negocio, sino también gestionar la evolución de la base de datos. A medida que un proyecto crece, mantener
Gestión de Migraciones de Base de Datos con Flyway en Spring Boot"
- Mauricio ECR
- Persistencia
- 14 Apr, 2025
Introducción
El desarrollo de aplicaciones modernas no solo implica escribir código de negocio, sino también gestionar la evolución de la base de datos. A medida que un proyecto crece, mantener la coherencia del esquema entre desarrolladores, ramas y entornos puede volverse complejo.
Aquí es donde Flyway entra en juego: una herramienta de migración de base de datos ligera y poderosa que permite controlar versiones de esquemas de forma segura, repetible y automatizada.
¿Qué es Flyway?
Flyway es una herramienta de migración de base de datos que permite aplicar scripts de manera controlada y automática. Utiliza una convención de nombres para identificar versiones y aplica cambios incrementales cada vez que la aplicación se inicia.
Problemas que resuelve:
- Desincronización entre esquemas de desarrollo, prueba y producción.
- Cambios accidentales o no versionados.
- Dificultad para aplicar migraciones en equipo o CI/CD.
- Fragilidad de los esquemas generados automáticamente por JPA.
Comparación breve con alternativas:
| Herramienta | Lenguaje | Comunidad | SQL puro | Migraciones Java |
|---|---|---|---|---|
| Flyway | Java | Muy activa | ✅ Sí | ✅ Opcional |
| Liquibase | Java | Activa | ✅ Sí | ✅ Más flexible |
¿Cuándo usar Flyway?
Escenarios ideales:
- Proyectos con evolución frecuente del esquema.
- Equipos distribuidos o con múltiples entornos (dev, test, prod).
- Necesidad de auditoría o trazabilidad de cambios en el esquema.
Ventajas sobre auto-DDL de JPA (spring.jpa.hibernate.ddl-auto):
- Evita sobrescritura accidental de datos.
- Versionado explícito de cambios.
- Mayor control y trazabilidad de la evolución del esquema.
Implementación en Spring Boot
Requisitos previos:
Agrega las siguientes dependencias en tu archivo pom.xml:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
</dependency>
</dependencies>
Para Gradle:
implementation 'org.flywaydb:flyway-core'
Configuración básica (application.properties):
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
spring.jpa.hibernate.ddl-auto=none
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
Ejemplo Práctico: Proyecto de Gestión de Usuarios
Supongamos una aplicación con una tabla users. Vamos a construir el esquema paso a paso usando Flyway.
Estructura del proyecto:
src/
└── main/
└── resources/
└── db/
└── migration/
├── V1__Create_user_table.sql
└── V2__Add_user_role_column.sql
Primera Iteración – Crear tabla users
Archivo: V1__Create_user_table.sql
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL,
email VARCHAR(100) UNIQUE
);
✅ Al iniciar la aplicación, Flyway detecta este archivo y lo ejecuta. Marca la versión como aplicada en su propia tabla de control (flyway_schema_history).
Segunda Iteración – Agregar columna role
Archivo: V2__Add_user_role_column.sql
ALTER TABLE users ADD COLUMN role VARCHAR(20) DEFAULT 'USER';
✅ Flyway identifica que esta versión aún no ha sido aplicada, la ejecuta y actualiza su historial. No vuelve a aplicar la versión 1.
Visualización del Estado de la Base de Datos Tras las Migraciones
Después de ejecutar las dos migraciones (V1 y V2), Flyway deja una huella en la base de datos que te permite auditar el estado de los cambios.
Tablas creadas tras las migraciones:
1. Tabla de usuarios (users):
SELECT * FROM users;
Estructura:
| Columna | Tipo | Restricciones |
|---|---|---|
| id | BIGINT | PRIMARY KEY |
| username | VARCHAR(50) | NOT NULL |
| VARCHAR(100) | UNIQUE | |
| role | VARCHAR(20) | DEFAULT 'USER' |
2. Tabla de control de Flyway (flyway_schema_history):
SELECT * FROM flyway_schema_history;
Ejemplo de contenido:
| installed_rank | version | description | type | script | success |
|---|---|---|---|---|---|
| 1 | 1 | Create user table | SQL | V1__Create_user_table.sql | true |
| 2 | 2 | Add user role column | SQL | V2__Add_user_role_column.sql | true |
Migraciones Java-based
Aunque Flyway trabaja perfectamente con scripts SQL, en algunos casos puede ser útil definir migraciones programáticamente en Java. Esto es útil cuando:
- Necesitas lógica condicional o control de flujo.
- Quieres reutilizar servicios de Spring.
- Trabajas con bases de datos no relacionales o lógicas avanzadas.
Cómo crear una migración Java:
- Implementa la clase extendiendo
BaseJavaMigration. - Ubícala en el paquete
db.migrationo configura la ubicación. - Nómbrala con el patrón
V{n}__Descripción.
Ejemplo: Crear tabla de auditoría
package db.migration;
import org.flywaydb.core.api.migration.BaseJavaMigration;
import org.flywaydb.core.api.migration.Context;
import java.sql.Statement;
public class V3__Create_audit_table extends BaseJavaMigration {
@Override
public void migrate(Context context) throws Exception {
try (Statement stmt = context.getConnection().createStatement()) {
stmt.execute("""
CREATE TABLE audit (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
action VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
""");
}
}
}
Configuración adicional si se cambia la ubicación:
spring.flyway.locations=classpath:db/migration
spring.flyway.java-migrations-location=com.ejemplo.migraciones
Tabla Resumen
| Concepto | Descripción |
|---|---|
| Migraciones | Archivos SQL con prefijo V{n}__ que modifican el esquema. |
| Integración con Spring | Ejecuta automáticamente migraciones al iniciar la app. |
| Ubicación por defecto | classpath:db/migration |
| Uso recomendado | Proyectos con cambios frecuentes en el esquema y colaboración en equipo. |
Conclusión
Flyway no solo gestiona migraciones de manera declarativa (con SQL), sino que también ofrece una vía programática potente para casos avanzados. Su integración con Spring Boot hace que los cambios de esquema sean seguros, trazables y consistentes.
Recomendaciones Finales:
- Nunca modifiques un script ya aplicado.
- Usa migraciones Java cuando lo SQL no sea suficiente.
- Verifica la tabla
flyway_schema_historypara diagnosticar errores o validar versiones.
Referencias
WebSockets Seguros en Spring Boot: Protege tus Conexionesen Tiempo Real
- Mauricio ECR
- Seguridad
- 04 Apr, 2025
Introducción En la era de las aplicaciones en tiempo real, los WebSockets se han convertido en una tecnología fundamental para crear experiencias interactivas. Sin embargo, su naturaleza persisten
WebSockets Seguros en Spring Boot: Protege tus Conexionesen Tiempo Real
- Mauricio ECR
- Seguridad
- 04 Apr, 2025
Introducción
En la era de las aplicaciones en tiempo real, los WebSockets se han convertido en una tecnología fundamental para crear experiencias interactivas. Sin embargo, su naturaleza persistente y bidireccional presenta desafíos únicos de seguridad.
¿Cómo garantizar que solo usuarios autenticados puedan establecer conexiones WebSocket?
¿Cómo proteger los mensajes intercambiados?
Este artículo presenta una implementación completa de WebSockets seguros en Spring Boot, con explicaciones detalladas de cada componente y su función en el sistema de seguridad.
🛠️ Implementación del Backend
1. Clase Principal de la Aplicación
Esta es la clase de entrada estándar para una aplicación Spring Boot, que inicia el contexto de la aplicación.
package com.mecr.sample.webSocketsSecurity;
@SpringBootApplication
public class WebSocketsSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(WebSocketsSecurityApplication.class, args);
}
}
2. Configuración de Seguridad
La clase SecurityConfig define las reglas de seguridad para la aplicación, incluyendo la protección de endpoints WebSocket, configuración CORS y autenticación básica.
package com.mecr.sample.webSocketsSecurity.security.config;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/ws/**").authenticated()
.anyRequest().permitAll()
)
.formLogin(withDefaults())
.logout(withDefaults());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://127.0.0.1:5500", "http://localhost:5500"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowCredentials(true);
config.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Requested-With"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
3. Interceptor de Autenticación para WebSockets
El AuthHandshakeInterceptor verifica la autenticación del usuario antes de permitir el establecimiento de la conexión WebSocket.
package com.mecr.sample.webSocketsSecurity.webSocket.security;
public class AuthHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
if (request instanceof ServletServerHttpRequest servletRequest) {
HttpSession session = servletRequest.getServletRequest().getSession(false);
if (session != null) {
SecurityContext context = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");
if (context != null && context.getAuthentication() != null && context.getAuthentication().isAuthenticated()) {
attributes.put("AUTH", context.getAuthentication());
System.out.println("🔐 Usuario autenticado en interceptor: " + context.getAuthentication().getName());
return true;
}
}
}
System.out.println("❌ WebSocket rechazado en interceptor (no autenticado)");
return false;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception exception) {
}
}
4. Manejador de WebSockets
MyWebSocketHandler gestiona los eventos del ciclo de vida de la conexión WebSocket y el procesamiento de mensajes.
package com.mecr.sample.webSocketsSecurity.webSocket.handler;
@Component
public class MyWebSocketHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
Authentication authentication = (Authentication) session.getAttributes().get("AUTH");
if (authentication == null || !authentication.isAuthenticated()) {
System.out.println("❌ WebSocket rechazado: Usuario no autenticado");
session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
System.out.println("✅ WebSocket conectado: " + authentication.getName());
session.sendMessage(new TextMessage("Conexión exitosa! Bienvenido " + authentication.getName()));
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
Authentication authentication = (Authentication) session.getAttributes().get("AUTH");
if (authentication == null || !authentication.isAuthenticated()) {
session.close(CloseStatus.NOT_ACCEPTABLE);
return;
}
System.out.println("📩 Mensaje recibido de " + authentication.getName() + ": " + message.getPayload());
session.sendMessage(new TextMessage("Echo: " + message.getPayload()));
}
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
System.err.println("❌ Error en WebSocket: " + exception.getMessage());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
System.out.println("🚪 WebSocket cerrado");
}
}
5. Configuración de WebSockets
WebSocketConfig registra el manejador WebSocket y configura los interceptores necesarios.
package com.mecr.sample.webSocketsSecurity.webSocket.config;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
private final MyWebSocketHandler myWebSocketHandler;
public WebSocketConfig(MyWebSocketHandler myWebSocketHandler) {
this.myWebSocketHandler = myWebSocketHandler;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(myWebSocketHandler, "/ws")
.setAllowedOrigins("http://localhost:5500","http://127.0.0.1:5500")
.addInterceptors(new HttpSessionHandshakeInterceptor(), new AuthHandshakeInterceptor());
}
}
💻 Implementación del Frontend
Esta página HTML demuestra cómo interactuar con el backend seguro desde el navegador.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<title>WebSocket con Seguridad</title>
</head>
<body>
<h2>WebSocket Seguro</h2>
<button onclick="login()">Iniciar Sesión</button>
<button onclick="connectWebSocket()">Conectar WebSocket</button>
<button onclick="sendMessage()">Enviar Mensaje</button>
<p id="output"></p>
<script>
var socket;
function login() {
fetch("http://127.0.0.1:8080/login", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: "username=admin&password=admin",
credentials: "include"
}).then(response => {
if (response.ok) {
alert("Autenticado correctamente");
} else {
alert("Error de autenticación");
}
}).catch(error => console.error("Error:", error));
}
function connectWebSocket() {
socket = new WebSocket("ws://127.0.0.1:8080/ws");
socket.onopen = function() {
console.log("✅ WebSocket conectado!");
socket.send("Hola servidor!");
};
socket.onmessage = function(event) {
document.getElementById("output").innerText = "Respuesta del servidor: " + event.data;
};
socket.onerror = function(error) {
console.error("❌ Error en WebSocket:", error);
};
socket.onclose = function() {
console.log("🚪 WebSocket desconectado");
};
}
function sendMessage() {
if (socket) {
socket.send("Hola desde cliente!");
} else {
alert("Conéctate primero!");
}
}
</script>
</body>
</html>
✅ Conclusión
Esta implementación proporciona una base sólida para aplicaciones que requieren WebSockets seguros en Spring Boot. La combinación de Spring Security con interceptores personalizados garantiza que solo usuarios autenticados puedan establecer conexiones WebSocket, mientras que la configuración de CORS protege contra solicitudes no autorizadas desde otros dominios.
Los componentes clave trabajan juntos para ofrecer:
- Autenticación previa al handshake WebSocket
- Mantenimiento del contexto de seguridad durante la sesión
- Protección contra CSRF (aunque desactivada para WebSockets)
- Configuración CORS segura
- Manejo adecuado de errores y cierre de conexiones