GitHub - lsh2613/spring-rabbitmq: Spring + RabbitMQ를 활용한 1:1 채팅방 구현
Spring + RabbitMQ를 활용한 1:1 채팅방 구현. Contribute to lsh2613/spring-rabbitmq development by creating an account on GitHub.
github.com
1. 인증 방법
이 프로젝트에서는 JWT + Session을 적용하였다.
정확히 인증은 JWT를 활용한다
2. JWT는 Session 대신 사용하는 건데 왜 굳이 둘 다 사용할까?
먼저 JWT의 인증 방식에 대해 알아 보자
프로젝트마다 약간 다르겠지만 본인이 구현하는 방식은 아래와 같다
HTTP 요청 -> JwtAuthenticationFilter -> header의 token 추출 -> 유효성 검사 및 memberId 추출 -> SecurityContextHolder에 저장 -> member가 필요한 api에서 꺼내어 사용
여기서 중요한 점은 SecurityConextHolder는 Http Request를 처리하기 위해 하나의 스레드를 할당받고 SecurityConextHolder에 저장되는 authentication은 ThreadLocal에 저장되었다가 요청이 처리되면 스레드를 반환한다.
즉, http 요청이 처리되어 스레드가 반환되면 SecurityConextHolder에 저장된 authentication(memberId)를 사용할 수 없다는 뜻이다.
그럼 이게 왜 중요하다는 걸까?
HTTP와 STOMP의 동작 방식을 먼저 살펴보면 연결 수립하는 핸드셰이크 과정에서 HTTP를 사용하고 이후에는 WS frame을 통해 통신한다.
'나는 stomp 연결 할 때 jwt의 유효성만 검증하겠다' 하면은 STOMP-CONNECT 시 헤더에 jwt를 넣고 ChannelInterceptor나 @EventListner를 활용하여 유효성 검사가 되도록 구현하면 된다.
하지만 나의 프로젝트에서는 stomp가 connect 될 때 memberId, chatRoomId을 받아와 채팅방 입장 처리를 해야 하고, disconnect 될 때 채팅방 퇴장 처리를 해야 한다. 이를 http-jwt 인증 방식 그대로 SecurityContextHolder에 저장해두고 사용하게 되면 disconnect 시 데이터를 가져올 수 없다(stomp는 http가 아니기 때문). 또한, disconnect는 header를 추가할 수 없어 ws이 연결되어 있는 동안 memberId, chatRoomId를 저장해둬야 한다.
이 데이터들은 채팅방을 접속해 있는 동안에만 사용되어 데이터 수명이 짧고, 채팅을 보내는 과정에서 빈번히 사용된다는 특징이 있기 때문에 영구 db를 사용하는 것보다는 데이터 생명 주기에 맞춰 stomp-session에 관리하기로 결정했다.
http-session이 아닌 stomp-session이다.
3. 설계
앞서 언급했 듯이 stomp 메시지는 ChannelInterceptor나 @EventListner을 통해 잡을 수 있다
나의 프로젝트 에서는 @EventListner를 통해 채팅방 입장/퇴장 로직을 처리하고 있다
// ChatMessageController
@EventListener
public void handleWebSocketConnectListener(SessionConnectEvent event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
chatMessageService.handleConnectMessage(accessor);
}
@EventListener
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(event.getMessage());
chatMessageService.handleDisconnectMessage(accessor);
}
따라서 jwt 검증과 채팅방 입장/퇴장 로직을 분리하기 위해 위 코드를 그대로 냅두고 ChannelInterceptor를 통해 jwt 검증을 처리할 생각이다
ChannelInterceptor.preSend(Message<?> message, MessageChannel channel)를 오버라이딩 하면 stomp-connect 시 jwt를 검증하고 session에 memberId와 chatRoomId를 저장할 수 있다
4. 구현
JwtAuthenticationInterceptor
@Component
@RequiredArgsConstructor
public class JwtAuthenticationInterceptor implements ChannelInterceptor {
private final TokenUtil tokenUtil;
private final StompHeaderAccessorUtil stompHeaderAccessorUtil;
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);
if (accessor.getCommand() == CONNECT) {
String token = stompHeaderAccessorUtil.extractToken(accessor, TokenType.ACCESS_TOKEN);
Long memberId = tokenUtil.validateTokenAndGetMemberId(token);
stompHeaderAccessorUtil.setMemberIdInSession(accessor, memberId);
Long chatRoomId = stompHeaderAccessorUtil.getChatRoomIdInHeader(accessor);
stompHeaderAccessorUtil.setChatRoomIdInSession(accessor, chatRoomId);
}
return message;
}
}
서버로 메시지가 전달 받을 때 인증을 받아야 하니까 WebSockt의 InboundChannel로 설정해준다
WebSocketConfig
@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final JwtAuthenticationInterceptor jwtAuthenticationInterceptor;
...
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
registration.interceptors(jwtAuthenticationInterceptor);
}
}
'Project > RabbitMQ(STOMP)를 적용한 1:1 채팅' 카테고리의 다른 글
Spring + RabbitMQ를 통한 1:1 채팅방 구현 - 읽음/안읽음 적용 (3) (2) | 2024.11.16 |
---|---|
Spring + RabbitMQ를 통한 1:1 채팅방 구현 - 채팅내역을 MongoDB로 (2) (0) | 2024.11.01 |
Spring + RabbitMQ를 통한 1:1 채팅방 구현 (1) (0) | 2024.10.31 |