Skip to content

Websocket Session Handle

Mambo edited this page Apr 20, 2026 · 5 revisions
  • WebSocketMessageBrokerConfigurer.configureWebSocketTransport -> WebSocketTransportRegistration.addDecoratorFactory -> WebSocketHandlerDecoratorFactory.decorator
  • SubProtocolWebSocketHandler X

https://docs.spring.io/spring-framework/reference/web/websocket/stomp/authentication-token-based.html

image
package com.xxx.websocket.interceptor;

import com.xxx.bean.User;
import com.xxx.service.JwtTokenService;
import com.xxx.websocket.bean.UserPrincipal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.Optional;


/**
 * WebSocket 연결 시 JWT 토큰을 검증하고 사용자 정보를 Principal로 주입하는 인터셉터
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketAuthInterceptor implements ChannelInterceptor {

    private final JwtTokenService jwtTokenService;

    private static final String HEADER_AUTH = "Authorization";
    private static final String TOKEN_PREFIX = "Bearer ";

    @Override
    @SuppressWarnings("unchecked")
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

        if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
            Map<String, List<String>> nativeHeaders = (Map<String, List<String>>) message.getHeaders().get("nativeHeaders");
            
            try {
                String token = resolveToken(nativeHeaders)
                    .orElseThrow(() -> new MessageDeliveryException("Authentication failed. Invalid token."));

                User user = jwtTokenService.getUser(token);
                if (user == null) {
                    throw new MessageDeliveryException("Authentication failed. User account not found.");
                }

                log.info("[WebSocket] CONNECT - session: {}, user: {}", accessor.getSessionId(), user.getId());
                accessor.setUser(new UserPrincipal(user));
            } catch (Exception e) {
                log.warn("[WebSocket] Authentication failed in CONNECT: {}", e.getMessage());
            }
        }
        return message;
    }

    /**
     * 헤더에서 Bearer 토큰을 추출합니다.
     *
     * @param headers Native 헤더 맵
     * @return 추출된 토큰 (Optional)
     */
    private Optional<String> resolveToken(Map<String, List<String>> headers) {
        return Optional.ofNullable(headers)
            .map(h -> h.get(HEADER_AUTH))
            .filter(auth -> !auth.isEmpty())
            .map(auth -> auth.get(0))
            .map(token -> StringUtils.removeStart(token, TOKEN_PREFIX))
            .map(token -> StringUtils.stripEnd(token, ";"))
            .filter(token -> StringUtils.isNotBlank(token) && !"null".equalsIgnoreCase(token));
    }
}
  • StompEndpointRegistry 에 등록하는 HandshakeInterceptor 는 쿠키로 보내야만 인증이 가능
  • ChannelRegistration 에 등록하는 ChannelInterceptor 는 STOMP의 CONNECT 프레임이 전송되는 시점에 동작
  • 클라이언에서 STOMP 헤더로 토큰을 보내고 있으므로 HandshakeInterceptor 보다는 ChannelInterceptor 가 적합

연결 과정 (Connection Flow)

단계 발생 이벤트 관련 클래스 / 메서드 주요 역할
1단계 HTTPHandshake StompEndpointRegistry 클라이언트가 ws-stomp 경로로 연결 요청. HTTP를 WebSocket으로 업그레이드.
2단계 Raw Socket Open WebSocketHandlerDecorator 로우 레벨의 소켓이 물리적으로 연결됨. afterConnectionEstablished가 실행되며 메모리 저장소에 세션 ID 등록.
3단계 STOMP CONNECT WebSocketAuthInterceptor 클라이언트가 내보낸 **STOMP 프레임(헤더에 토큰 포함)**이 도착. 여기서 토큰을 검증하고 성공 시 Principal을 주입.
4단계 Connection Event WebSocketEventListener 스프링 애플리케이션 내부에 SessionConnectEvent 발생. 주입된 Principal 정보를 기반으로 사용자별 세션 맵핑 처리.

해제 과정 (Disconnection Flow)

단계 발생 이벤트 관련 클래스 / 메서드 주요 역할
1단계 STOMP DISCONNECT WebSocketEventListener 클라이언트가 정상적으로 종료 요청을 보냄. SessionDisconnectEvent 수신 및 관련 리소스 정리.
2단계 Raw Socket Close WebSocketHandlerDecorator 소켓이 물리적으로 닫힘. afterConnectionClosed가 실행되며 메모리 저장소에서 세션 삭제.
강제종료 Manual Close WebSocketSessionRepository 서버 측에서 토큰 만료 등으로 세션을 찾아 session.close() 호출 시 즉시 2단계로 이동.

Clone this wiki locally