
이제 본격적으로 WebSocket을 이용한 실시간 채팅을 구현해보자.
implementation 'org.springframework.boot:spring-boot-starter-websocket'
@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Value("${react.url}")
private String reactUrl;
// Simple Message Broker 활성화, Subscriber들에게 메시지를 전달
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins(reactUrl)
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/subscribe");
registry.setApplicationDestinationPrefixes("/publish");
}
}
@EnableWebSocketMessageBroker는 Spring WebSocket 메시징 기능을 활성화하는 어노테이션으로 WebSocket 연결을 처리하고 STOMP 프로토콜을 사용할 수 있게 한다.
"/ws" 경로를 WebSocket 연결을 위한 엔드포인트로 등록하고 setAllowedOrigins(reactUrl)를 통해 React 애플리케이션의 접근을 허용한다.
- 클라이언트는 "/publish" 주소로 메시지 전송
- Spring WebSocket 메시징 시스템이 이 메시지를 받아 내장 Simple Message Broker로 전달
- Simple Message Broker는 메시지를 "/subscribe" 주소로 구독 중인 클라이언트들에게 전송
현재 A1BnB는 JWT 토큰을 통해 인증된 사용자 정보를 조회하고 있다. 따라서 웹소켓 통신 시 사용자 정보를 받으려면 별도의 설정을 해줘야 한다.
Spring Security Messaging는 WebSocket 및 STOMP 프로토콜을 사용하는 애플리케이션에 대한 보안 기능을 제공한다.
implementation 'org.springframework.security:spring-security-messaging'
AbstractSecurityWebSocketMessageBrokerConfigurer를 상속받아 WebSocket 보안 설정을 구현할 수 있다.
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(MessageSecurityMetadataSourceRegistry message) {
message
.nullDestMatcher().permitAll()
.simpDestMatchers("/publish/**").authenticated()
.simpSubscribeDestMatchers("/subscribe/**").authenticated()
.anyMessage().denyAll();
}
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
ChannelInterceptor 인터페이스를 구현하여 WebSocket 메시지 처리 과정을 커스터마이징할 수 있다.
@Component
@RequiredArgsConstructor
public class WebSocketInterceptor implements ChannelInterceptor {
private final SecurityService securityService;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor.getCommand() == StompCommand.CONNECT) {
String token = accessor.getFirstNativeHeader(HEADER_STRING.getValue()).replace(TOKEN_PREFIX.getValue(), "");
// 토큰 검증
securityService.validateToken(token);
// 인증 정보 주입
Authentication authentication = securityService.extractAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
return message;
}
}
커스텀한 preSend() 메서드는 다음과 같이 동작한다.
- WebSocket 연결 요청 수신받으면 preSend() 메서드가 호출
- STOMP 헤더에서 토큰을 추출
- 토큰 검증
- 검증 성공 시 토큰에서 인증 정보 추출
- 추출한 인증 정보를 SecurityContextHolder에 설정
- StompHeaderAccessor의 setUser() 메서드를 호출하여 인증 정보를 STOMP 헤더에 추가
구현한 WebSocketInterceptor는 configureClientInboundChannel 메서드를 통해 등록할 수 있다.
@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final WebSocketInterceptor webSocketInterceptor;
@Value("${react.url}")
private String reactUrl;
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws")
.setAllowedOrigins(reactUrl)
.withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.enableSimpleBroker("/subscribe");
registry.setApplicationDestinationPrefixes("/publish");
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(webSocketInterceptor);
}
}
브라우저의 콘솔 창을 확인해보면 다음과 같이 헤더에 토큰을 담고 연결 요청 시 사용자 정보를 불러오는 것을 확인할 수 있다.
