본문 바로가기
WEB/Spring

[Spring] 웹소켓(Websocket) 개념과 예제

by 정권이 내 2023. 8. 11.

[Spring] 웹소켓(Websocket) 개념과 예제

 

웹소켓(Websocket) 이란

img

이미지 출처: https://www.vaadata.com/blog/websockets-security-attacks-risks/

 

웹소켓(WebSockets)은 양방향 통신을 지원하는 네트워크 프로토콜로, 클라이언트와 서버 간의 실시간 데이터 전송을 위해 사용됩니다. 기존의 HTTP 프로토콜은 클라이언트가 서버에 요청을 보내고 서버가 응답을 반환하는 단방향 통신을 지원하였지만, 웹소켓은 이와 달리 양방향 통신을 가능하게 해줍니다.

웹소켓은 주로 웹 애플리케이션에서 실시간 통신을 구현하는 데 사용되며, 대부분의 최신 브라우저와 서버 측 언어에서 지원됩니다.JavaScript를 사용하여 클라이언트 측에서 웹소켓을 다루는 것이 일반적이며, 서버 측에서는 여러 언어와 프레임워크에서 웹소켓 서버를 구현할 수 있습니다.

 

1. 웹소켓의 특징

  • 양방향 통신: 웹소켓은 클라이언트와 서버 간에 데이터를 양방향으로 실시간으로 주고받을 수 있습니다. 이를 통해 실시간 채팅, 멀티플레이어 게임, 주식 시세 업데이트 등 다양한 응용 프로그램을 개발할 수 있습니다.
  • 낮은 지연: 웹소켓은 TCP 연결을 사용하며, 연결을 유지한 상태에서 데이터를 교환하기 때문에 데이터 전송에 있어서 높은 지연 없이 실시간 통신이 가능합니다.
  • 단일 연결 유지: 웹소켓은 클라이언트와 서버 간의 단일 연결을 유지하면서 여러 번의 메시지 교환을 처리할 수 있습니다. 이는 HTTP와 달리 매번 새로운 연결을 맺지 않아도 되므로 연결 설정의 오버헤드를 줄여줍니다.
  • 프로토콜 확장 가능: 웹소켓은 초기에 정의된 프로토콜을 기반으로 하며, 필요한 경우 확장하여 추가적인 기능을 지원할 수 있습니다.

 

2. 웹소켓 통신 절차

  1. 클라이언트는 서버에 웹소켓 연결을 요청합니다.
  2. 서버는 웹소켓 연결을 수락하고 클라이언트와의 양방향 통신을 시작합니다.
  3. 클라이언트와 서버는 메시지를 주고받으면서 실시간 데이터를 교환합니다.
  4. 클라이언트 또는 서버가 연결을 종료하려면 연결을 닫을 수 있습니다.

 

웹소켓 관련 용어 이해

메시지 브로커(Message Broker)

  • 메시지 기반의 통신에서 중간에 위치하여 메시지의 송수신을 관리하고 조정하는 시스템이나 컴포넌트를 말합니다. 메시지 브로커는 다수의 클라이언트나 서버 간에 메시지를 안전하고 효율적으로 전달하기 위한 역할을 수행합니다.

STOMP(Streaming Text Oriented Messaging Protocol) 프로토콜

  • 실시간 메시징 시스템을 위한 간단하고 텍스트 기반의 프로토콜입니다. STOMP는 웹소켓을 비롯한 다양한 메시징 시스템에서 사용되며, 메시지 브로커와 클라이언트 간의 상호 작용을 효과적으로 구성할 수 있도록 설계되었습니다.

img

이미지 출처: https://www.toptal.com/java/stomp-spring-boot-websocket

 

웹소켓 보안 취약점과 대응 방식

웹소켓은 강력한 실시간 통신 기능을 제공하지만, 잘못 사용되었을 때 보안 취약점이 발생할 수 있습니다. 몇 가지 주요한 웹소켓 보안 취약점과 대응 방법을 알아보겠습니다.

 

CORS(Cross-Origin Resource Sharing) 공격

  • 취약점: 악의적인 사용자가 다른 도메인에서 웹소켓 연결을 시도하여 개인 정보 노출 및 CSRF(Cross-Site Request Forgery) 공격을 수행할 수 있습니다.
  • 대응 방법: 적절한 CORS 설정을 적용하여, 특정 도메인만 웹소켓 연결을 허용하도록 제한합니다. 또한 인증된 사용자만 웹소켓 연결을 허용하도록 인증 절차를 강화합니다.

 

데이터 유출 및 암호화 부재

  • 취약점: 웹소켓 연결이 암호화되지 않으면 데이터가 중간에 노출될 수 있습니다.
  • 대응 방법: 웹소켓 연결을 HTTPS(SSL/TLS) 위에서 작동하도록 설정하여 데이터의 암호화를 보장합니다.

 

DDoS (Distributed Denial of Service) 공격

  • 취약점: 악의적인 사용자가 다수의 연결을 만들어 서버를 과부하시키는 DDoS 공격이 발생할 수 있습니다.
  • 대응 방법: 웹소켓 연결 수 제한, IP 주소 필터링, 요청 검증 등을 통해 DDoS 공격을 방어합니다.

 

세션 관리 부재

  • 취약점: 웹소켓 연결의 세션 관리가 제대로 이루어지지 않으면 인증 및 권한 부여가 무시될 수 있습니다.
  • 대응 방법: 웹소켓 연결의 세션 관리를 효과적으로 구현하여 인증 정보를 유지하고 권한 부여를 확인합니다.

 

XSS (Cross-Site Scripting) 공격

  • 취약점: 악의적인 스크립트가 웹소켓 메시지로 전달되어 사용자 브라우저에서 실행될 수 있습니다.
  • 대응 방법: 입력 데이터의 유효성 검사와 이스케이프 처리를 통해 XSS 공격을 방어합니다.

 

WebSocket DoS (Denial of Service) 공격

  • 취약점: 웹소켓 연결을 열고 열고 계속해서 연결을 유지하여 서버의 자원을 고갈시키는 공격이 발생할 수 있습니다.
  • 대응 방법: 연결 시간 제한을 설정하거나, 특정 시간 내에 허용 가능한 연결 수를 제한하여 DoS 공격을 방어합니다.

 

웹소켓 예제코드

이제 간단한 채팅 어플리케이션 예제를 만들어보겠습니다.

 

WebSocketConfig.java

  • Spring에서 WebSocket을 활성화 하는 클래스(Configuration)
  • @EnableWebSocketMessageBroker 어노테이션의 역할은 Spring Framework에서 WebSocket을 사용하여 메시지 브로커와 통신을 설정, 관리하기 위한 역할을 합니다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig  implements WebSocketMessageBrokerConfigurer {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

        // 인가(접근권한) 설정
        http.authorizeHttpRequests().antMatchers("/").permitAll();
        http.authorizeHttpRequests().antMatchers("/admin/**").hasRole("admin");
        http.authorizeHttpRequests().antMatchers("/member/**").hasRole("user");

        // 사이트 위변조 요청 방지
        http.csrf().disable();

        return http.build();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/topic");
        registry.setApplicationDestinationPrefixes("/app");
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/chat").withSockJS();

    }
}

 

WebSocketHandler.java

  • 메시지 컨트롤러 클래스
@Controller
public class WebSocketHandler {

    @MessageMapping("/chat")
    @SendTo("/topic/messages")
    public OutputMessage greeting(Message message) {
        String time = new SimpleDateFormat("HH:mm").format(new Date());
        return new OutputMessage(message.getFrom(), message.getText(), time);
    }
}

 

index.html

<!DOCTYPE html>
<html>
<head>
    <title>Chat WebSocket</title>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
    <style>
        body {
            background-color: #f8f9fa;
        }
        .chat-container {
            margin: auto;
            margin-top: 50px;
            max-width: 600px;
            border: 1px solid #ccc;
            padding: 20px;
            background-color: white;
            border-radius: 5px;
        }
        .chat-messages {
            max-height: 600px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            background-color: #f5f5f5;
            height: auto;
        }
        .input-group {
            margin-top: 10px;
        }
        #text {
            width: 80%;
        }
    </style>
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script type="text/javascript" src="/chat.js"></script>
</head>
<body onload="disconnect()">
<div class="chat-container">
    <div class="input-group">
        <input type="text" id="from" class="form-control" placeholder="Choose a nickname"/>
    </div>
    <div class="input-group">
        <button id="connect" class="btn btn-primary" onclick="connect();">Connect</button>
        <button id="disconnect" class="btn btn-secondary" disabled="disabled" onclick="disconnect();">
            Disconnect
        </button>
    </div>
    <div class="input-group">
        <input type="text" height="auto" id="text" class="form-control" placeholder="Write a message..."/>
        <div id="conversationDiv" class="input-group-append">
            <button id="sendMessage" class="btn btn-success" onclick="sendMessage();">Send</button>
        </div>
    </div>
    <div class="chat-messages" >
        <p id="response"></p>
    </div>
</div>
</body>
</html>

 

chat.js

var stompClient = null;

    function setConnected(connected) {
        document.getElementById('connect').disabled = connected;
        document.getElementById('disconnect').disabled = !connected;
        document.getElementById('conversationDiv').style.visibility
          = connected ? 'visible' : 'hidden';
        document.getElementById('response').innerHTML = '';
    }

    function connect() {
        var socket = new SockJS('/chat');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, function(frame) {
            setConnected(true);
            console.log('Connected: ' + frame);
            stompClient.subscribe('/topic/messages', function(messageOutput) {
                showMessageOutput(JSON.parse(messageOutput.body));
            });
        });
    }

    function disconnect() {
        if(stompClient != null) {
            stompClient.disconnect();
        }
        setConnected(false);
        console.log("Disconnected");
    }

    function sendMessage() {
        var from = document.getElementById('from').value;
        var text = document.getElementById('text').value;
        stompClient.send("/app/chat", {},
          JSON.stringify({'from':from, 'text':text}));
    }

    function showMessageOutput(messageOutput) {
        var response = document.getElementById('response');
        var p = document.createElement('p');
        p.style.wordWrap = 'break-word';
        p.appendChild(document.createTextNode(messageOutput.from + ": "
          + messageOutput.text + " (" + messageOutput.time + ")"));
        response.appendChild(p);
    }

 

WebSocketConfig 는 웹소켓을 활성화 하는 클래스이며 WebSocketMessageBrokerConfigurer 인터페이스를 재정의한 메서드들의 의미를 알아보겠습니다.

@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
    registry.enableSimpleBroker("/topic");
    registry.setApplicationDestinationPrefixes("/app");
}

@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
    registry.addEndpoint("/chat").withSockJS();

}

 

configureMessageBroker(MessageBrokerRegistry registry)

  • 이 메서드는 메시지 브로커 설정을 구성하는 역할을 합니다. 메시지 브로커는 클라이언트 간의 메시지 전달을 중개하고 관리합니다.
  • enableSimpleBroker("/topic"): 메시지 브로커를 활성화하고, 클라이언트가 "/topic"으로 시작하는 주제에 구독하면 해당 주제로 발행되는 메시지를 받을 수 있도록 합니다.
  • setApplicationDestinationPrefixes("/app"): 클라이언트에서 메시지를 보낼 때 사용할 애플리케이션 접두사를 설정합니다. 이 예제에서는 "/app"으로 시작하는 주소를 사용하여 메시지를 전송합니다.

 

registerStompEndpoints(StompEndpointRegistry registry)

  • 이 메서드는 웹소켓 연결 엔드포인트를 등록하는 역할을 합니다. 클라이언트가 이 엔드포인트를 통해 웹소켓 연결을 시도할 수 있습니다.
  • addEndpoint("/chat"): "/chat" 이라는 웹소켓 연결 엔드포인트를 등록합니다.
  • setAllowedOrigins("http://localhost:8080"): 특정 출처에서만 웹소켓 연결을 허용합니다. ("*"은 모두 허용)
  • withSockJS(): SockJS를 사용하여 웹소켓 연결을 지원합니다. SockJS는 웹소켓이 지원되지 않는 환경에서 폴백 기능을 제공하여 연결을 유지할 수 있게 합니다.

 

위의 설정으로 클라이언트는 /app 접두사로 메시지를 보내고, /topic으로 시작하는 주제에 구독하여 메시지를 수신할 수 있습니다. /chat 엔드포인트를 통해 웹소켓 연결을 수립할 수 있으며, 필요한 경우 SockJS를 통한 폴백 기능도 활성화할 수 있습니다.

 

전체 코드는 github에 올렸으니 참고하시기 바랍니다.

 

GitHub - rlatjd1f/spring.websocket.demo

Contribute to rlatjd1f/spring.websocket.demo development by creating an account on GitHub.

github.com

 

웹소켓 예제 실행

브라우저 2개를 실행하여 localhost:8080에 접속하고 닉네임을 입력, Connect 버튼을 눌러 서버에 연결하고 채팅을 하면 실시간으로 내 채팅과 상대방 채팅이 출력되는것이 보입니다.

img

 

크롬 개발자모드에서 콘솔 로그를 확인해보면 실제 채팅 메시지가 어떻게 전달되는지 좀더 자세하게 볼수 있습니다. 아래 콘솔 로그 사진은 닉네임 apple을 사용하는 브라우저의 로그입니다.

img

  • apple 사용자의 콘솔 로그에서 먼저 채팅을 보낼때 /app/chat 경로를 목적지로 하여 메시지를 보내고 있습니다.
  • 서버에서 내가 보낸 메시지를 채팅 형식에 맞게 반환하여 UI에 출력합니다.
  • banana 사용자가 보낸 메시지가 서버를 통해 apple 사용자에게 수신됩니다.

 

이렇게 해서 Spring WebSocket의 기본 개념들과 간단한 예제를 통해 웹소켓이 어떻게 동작하는지 알아 보았습니다.

 

연관 포스팅

반응형

댓글