Type something to search...
WebSockets Seguros en Spring Boot: Protege tus Conexionesen Tiempo Real

WebSockets Seguros en Spring Boot: Protege tus Conexionesen Tiempo Real

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

Related Posts