Spring WebSocket과 STOMP를 이용해서 채팅 웹 어플리케이션을 구현하는 중,
사용자가 제한 인원이 꽉 찬 채팅방에 SUBSCRIBE 요청을 보낼 경우 서버에서 거절 메시지를 보내서 클라이언트에서 리다이렉트 등의 처리를 할 수 있도록 하고 싶었다.
이런 식으로 특정 유저를 차단하는 기능도 구현할 수 있을 것 같다.
먼저 ChannelInterceptorAdapter 을 상속받고 presend메소드를 오버라이딩 한다.
ChannelInterceptor 인터페이스를 구현해도 되지만 그렇게 할 경우 불필요한 메소드도 모두 구현해 주어야 하기 때문에, 편의상 ChannelInterceptorAdapter을 상속받아서 필요한 presend메소드만 오버라이딩 한다.
스프링 컨테이너에 TopicSubscriptionInterceptor을 인터셉터로 등록해 준다. 이렇게 함으로써 클라이언트로부터 오는 inbound message를 가로챌 수 있다.
package com.example.websocketdemo.config;
import com.example.websocketdemo.interceptor.TopicSubscriptionInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Autowired
TopicSubscriptionInterceptor topicSubscriptionInterceptor;
@Override
public void configureClientInboundChannel(ChannelRegistration registration){
registration.setInterceptors(topicSubscriptionInterceptor);
}
}
TopicSubscriptionInterceptor에 @Component 애노테이션을 붙여줘서 스프링 빈으로 등록 되도록 하고, ChatRoomRepository , MessageChannel 을 주입받는다.
@Slf4j
@Component
@RequiredArgsConstructor
public class TopicSubscriptionInterceptor extends ChannelInterceptorAdapter{
private final ChatRoomRepository chatRoomRepository;
private final MessageChannel clientOutboundChannel;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
if(StompCommand.SUBSCRIBE.equals(accessor.getCommand())){
MessageHeaders headers = message.getHeaders();
MultiValueMap<String, String> map= headers.get(StompHeaderAccessor.NATIVE_HEADERS, MultiValueMap.class);
String chatRoomId = map.getFirst("chatRoomId");
StompHeaderAccessor headerAccessor = StompHeaderAccessor.create(StompCommand.MESSAGE);
headerAccessor.setSessionId(accessor.getSessionId());
headerAccessor.setSubscriptionId(accessor.getSubscriptionId());
if (!validateSubscription(chatRoomId)) {
headerAccessor.setMessage("FULL");
clientOutboundChannel.send(MessageBuilder.createMessage(new byte[0], headerAccessor.getMessageHeaders()));
return null;
}else{
headerAccessor.setMessage("OK");
}
clientOutboundChannel.send(MessageBuilder.createMessage(new byte[0], headerAccessor.getMessageHeaders()));
}
return message;
}
private boolean validateSubscription(String chatRoomId) {
ChatRoom chatRoom = chatRoomRepository.findById(chatRoomId);
if(chatRoom != null && chatRoom.checkCount()){
return true;
}
return false;
}
}
SUBSCRIBE 요청일 때, 현재 채팅방 인원수를 확인하여 채팅방 인원수가 최대 인원 수보다 작다면, OK 응답을 보내고 현재 채팅방 인원수가 최대 인원수와 같다면 "FULL" 을 보낸다.
예외를 던지거나 StompCommand.MESSAGE를 ERROR로 하지 않는 이유는 그렇게 할 경우 클라이언트단에서 채팅방의 인원이 꽉 찼을 때 alert 하고 리다이렉트 하는 등의 처리를 할 수 없기 때문이다.
1) 메시지에 대한 정보를 얻기 위해 메시지를 Wrapping 해 StompMessageHeaderAccessor을 얻어낸다.
2) StompHeaderAccessor.create로 메시지를 만들고, 필수 정보(sessionId, setSubscriptionId) 등을 입력한다.
참고
STOMP 서버는 모든 subscriber들에게 메시지를 브로드캐스팅 하기 위해 MESSAGE COMMAND를 사용할수 있다.MESSAGE message-id:nxahklf6-1 subscription:sub-1 destination:/topic/price.stock.MMM {"ticker":"MMM","price":129.45}^@
서버의 모든 메시지는 특정 클라이언트 subscription에 응답해야만 하며, 서버 메시지의 subscription-id 헤더는 클라이언트가 subscribe한 id헤더와 일치해야 한다.
출처
3) 클라이언트에서 SUBSCRIBE 요청할 때 보낸 chatRoomId를 이용해 chatRoom을 찾고 현재 인원수를 확인한 다음 setMessage() 메서드를 통해 "FULL" 또는 "OK"를 보낸다.
4) 서버가 WebSocket 클라이언트에게 메시지 보내기 위한 Channel인 clientOutboundChannel의 send메소드를 이용해 메시지를 보낸다.
stompClient.subscribe(`/topic/chat/${chatRoomId}`,onMessageReceived, { chatRoomId : chatRoomId });
function onMessageReceived(payload) {
if(payload.headers.message === "FULL"){
alert('인원이 다 찼습니다!');
window.location.href = 'http://localhost:8080/chatRoom.html';
}else if(payload.headers.message === "OK"){
addUser();
return;
}
}
안녕하세요! 글 너무 잘 읽었습니다! 저도 비슷한 방식으로 interceptor를 구현했는데 실행시 bean 들간의 circular dependencies가 발생했다고 오류가 뜨던데 이것은 어떤식으로 처리하셨나요??