WebSocketMessageBrokerConfigurer (Interface)

@RequiredArgsConstructor
@Configuration
@EnableWebSocketMessageBroker // STOMP 기반 WebSocket 메시지 처리를 활성화합니다.
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final JwtStompInterceptor jwtStompInterceptor;
    
    /**
     * 클라이언트가 WebSocket 연결을 시작할 엔드포인트를 등록합니다.
     * SockJS를 사용하여 브라우저 호환성을 높입니다.
     */
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        // 클라이언트가 이 경로로 접속하여 Handshake를 시작합니다.
        // Spring Security는 이 HTTP Handshake 요청을 가로채서 인증을 처리합니다.
        registry.addEndpoint("/ws/chat")
                .setAllowedOriginPatterns("*") // CORS 설정 (* 대신 실제 도메인 권장)
                .withSockJS(); // SockJS 지원 활성화 (대부분의 브라우저에서 안정적인 연결을 위해 사용)
    }

    /**
     * 메시지 브로커(Message Broker)를 설정합니다.
     * 메시지를 어떤 Prefix로 라우팅할지 정의합니다.
     */
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        // 1. /app/* 으로 시작하는 메시지는 @Controller의 @MessageMapping 메서드로 라우팅됩니다.
        config.setApplicationDestinationPrefixes("/app");

        // 2. /topic, /queue 로 시작하는 메시지는 브로커가 처리합니다. (클라이언트에게 전송)
        // /topic: 1:N 공통 메시징 (채팅방, 공지 등)
        // /queue: 1:1 개인 메시징
        config.enableSimpleBroker("/topic", "/queue");

        // 3. 사용자 고유의 queue 경로 Prefix 설정 (개인 메시지 전송 시 사용)
        // 예를 들어, /user/queue/messages 경로로 전송된 메시지는 인증된 사용자에게만 전달됩니다.
        config.setUserDestinationPrefix("/user");
    }

    /**
     * 인바운드 STOMP 메시지(SEND, SUBSCRIBE 등)에 대한 인가 규칙을 설정합니다.
     * MessageMatcherDelegatingAuthorizationManager 빌더를 사용하여 람다 기반으로 규칙을 정의합니다.
     */
    @Bean
    public AuthorizationManager<Message<?>> messageAuthorizationManager() {

        // 람다를 사용한 AuthorizationManager 구성
        return MessageMatcherDelegatingAuthorizationManager.builder()

                // 1. 프로토콜 관련 메시지 타입(CONNECT, HEARTBEAT 등)은 모두 허용합니다.
                //    (JWT 인증은 StompHandler에서 이미 처리되므로 여기서 차단하지 않습니다.)
                .simpTypeMatchers(SimpMessageType.CONNECT,
                        SimpMessageType.HEARTBEAT,
                        SimpMessageType.DISCONNECT,
                        SimpMessageType.UNSUBSCRIBE)
                .permitAll()

                // 2. /app/* 목적지로 메시지를 보내는(SEND) 요청은 인증된 사용자만 허용합니다.
                .simpDestMatchers("/app/**").authenticated()

                // 3. /topic/*, /queue/* 목적지를 구독하는(SUBSCRIBE) 요청은 인증된 사용자만 허용합니다.
                .simpDestMatchers("/topic/**", "/queue/**").authenticated()

                // 4. 위에 명시되지 않은 모든 메시지는 거부하여 보안을 강화합니다.
                .anyMessage().denyAll()

                .build();
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        // jwtStompInterceptor를 STOMP 통신 채널의 인터셉터로 등록
        registration.interceptors(jwtStompInterceptor);
    }
}

@EnableWebSocketMessageBroker 이 어노테이션은 STOMP 기반 WebSocket 메시지 처리를 활성화 하는 기능을 한다.

WebSocketMessageBrokerConfigurer 이 인터페이스는 위 어노테이션으로 STOMP 기능을 켠 다음, 동작 방식을 개발자가 직접 커스터마이징할 수 있게 해주는 인터페이스이다.

항상 짝궁으로 등장 한다고 보면 된다.

public void registerStompEndpoints(StompEndpointRegistry registry) 오버라이드 한 이 매서드는 클라이언트가 /ws/chat 같은 경로로 WebSocket(SockJS) 연결을 시도할 때 그 요청을 HTTP로 먼저 받아 WebSocket으로 업그레이드(Handshake) 하도록 엔드포인트를 등록한다.

처음에는 HTTP 요청으로 들어오지만, 성공시 WebSocket 연결로 전환되는 구조.

public void configureMessageBroker(MessageBrokerRegistry config) 오버라이드 한 다음 매서드는 메시지 브로커를 설정하는 매서드로 클라이언트 ↔ 서버 간 메시지가 들어오고 나가는 경로(라우팅 규칙) 를 정의한다.

이 부분은 JS로 예를 들자면

.simpDestMatchers("/topic/**", "/queue/**").authenticated()
userListSub = stompClient.subscribe('/topic/users', msg => {
						      
						      // 요청이 들어오면 수행할 코드 추가 부분.
    });

이런식으로 구독이 진행되게 된다. 여기서 /topic 이 부분의 경로로 시작하는 경로로 구독 요청이 (SUBSCRIBE) 서버로 들어오게 된다.


ChannelInterceptor (Interface)

@Component
@RequiredArgsConstructor
public class JwtStompInterceptor implements ChannelInterceptor {

    private final JwtTokenProvider jwtTokenProvider;

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

        // STOMP 연결 요청일 때만 토큰 검증
        if (StompCommand.CONNECT.equals(accessor.getCommand())) {
            // Authorization 헤더에서 토큰 추출
            String authorizationHeader = accessor.getFirstNativeHeader("Authorization");
            if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
                throw new JwtValidationException("토큰이 존재하지 않거나 Bearer 타입이 아닙니다.");
            }
            String token = authorizationHeader.substring(7);

            // 토큰 유효성 검증
            jwtTokenProvider.validateToken(token);

            // 사용자 정보 설정
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            accessor.setUser(authentication);
        }

        return message;
    }
}

이 인터페이스는 Security와 STOMP를 연동할 수 있게 해주는 기반 구조”를 만들어주고 최초 연결뿐만 아니라 모든 STOMP 메시지 흐름에 개입할 수 있는 인터셉터를 구현할수 있다. (작성된 클래스는 최초 연결만 검사)

위에서 설명한 registerStompEndpoints() 매서드에서 실행하는 인증 과정을 정의 한다.