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() 매서드에서 실행하는 인증 과정을 정의 한다.