WebSockets Seguros en Spring Boot: Protege tus Conexionesen Tiempo Real
- Mauricio ECR
- Seguridad Web
- 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