웹소켓 채팅 전송시 권한 확인 방법
알아두기 - HTTP와 웹소켓의 인증과정에서의 차이점
HTTP 통신은 요청-응답 패러다임에 기반을 두고 있으며, 각 요청 후 연결이 종료됩니다. 따라서 매 요청 마다 새롭게 AccessToken을 검증해야 합니다. 이를 두고 Stateless 라고 합니다.
반면, 웹소켓은 최초 연결시에만 HTTP 요청을 사용하며, 연결이 맺어진 후에는 지속적인 데이터 교환을 제공하기에 추가적인 AccessToken 검증이 불필요해지게 됩니다. 이를 두고 Stateful이라고 하며, 웹소켓이 연결된 동안 인증과 관련된 정보를 웹소켓 세션에 유지 시켜 활용할 수 있게 됩니다.
Overview - 채팅 인증 절차
위 그림을 크게 두 가지 요청으로 나누어 설명드리겠습니다!
- CONNECT 요청 - 연결 요청 가장 먼저 웹소켓 연결 요청의 경우 HTTP요청을 사용합니다! 이때 handshake 과정을 거치며 webSocket 연결로 변경을 시도하게됩니다! 기존의 HTTP 요청과 마찬가지로 헤더에 AccessToken을 검증하도록 설계가 가능합니다!
이때 HandshakeInterceptor를 사용하여 인증 처리를 구현하도록합니다! 검증 과정을 거치며 웹소켓 세션 내에 멤버 정보와 사용가능한 채팅방의 정보를 저장하도록 합니다. 이 정보는 웹소켓 연결 종료시까지 유지됩니다.
- SEND 요청 - 메시지 전송 요청 연결 이후 부터 SEND 요청을 사용할 수 있습니다. 이때부터는 요청의 권한 검증을 위해 웹소켓 세션내에 있는 사용가능한 채팅방 정보와, 사용자가 보낸 payload의 채팅방 정보를 대조합니다.
이때는 ChannelInterceptor 를 사용하여 인가 처리를 구현합니다! 이후 문제가 없다면 해당 요청은 컨트롤러로 넘어가 처리됩니다.
해당 방법은 Stateful의 특성에 따라 제가 생각한 인증 구현 방법이며, 더 나은 의견이 있다면 언제든지 말씀해주시면 감사하겠습니다!
구현방법 - How to make
핸드 쉐이크 인터셉터 (HandshakeInterceptor)
초기 연결 시에는 HandshakeInterceptor를 사용하여 사용자 인증을 수행합니다.
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.server.HandshakeInterceptor;
import java.util.Map;
public class CustomHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
// 인증 로직 구현
// 토큰 검증, 사용자 식별, 채팅방 정보 불러오기(서비스 -> DB 접근) 등
return true; // 인증 성공 시 true 반환, 실패 시 false 반환
}
}
채널 인터셉터 (ChannelInterceptor)
연결 후 메시지 교환 단계에서는 ChannelInterceptor를 사용하여 각 메시지의 권한을 검증합니다. 이는 사용자가 권한이 있는 채팅방에만 메시지를 전송할 수 있도록 보장합니다.👍
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.support.ChannelInterceptor;
public class CustomChannelInterceptor implements ChannelInterceptor {
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
SimpMessageHeaderAccessor headerAccessor = SimpMessageHeaderAccessor.wrap(message);
if (StompCommand.SEND.equals(headerAccessor.getCommand())) {
// 메시지 권한 검증 로직 구현
// 사용자가 해당 채팅방에 권한이 있는지 확인, 웹소켓 세션과 payload 비교
}
return message;
}
}
세션 관리
웹소켓 세션 동안 사용자의 권한 및 채팅방 정보는 세션에 저장되며, 연결 종료 시 이 정보는 제거됩니다.🗑️
// 연결 인터셉터에서 세션 정보 설정
public boolean beforeHandshake(...) {
// 사용자 인증 후
attributes.put("userId", userId); // 사용자 ID 저장
attributes.put("chatRooms", chatRooms); // 사용자 채팅방 목록 저장
...
}
// 채널 인터셉터에서 세션 정보 접근
public Message<?> preSend(...) {
// 세션에서 사용자 정보 가져오기
String userId = (String) headerAccessor.getSessionAttributes().get("userId");
List<String> chatRooms = (List<String>) headerAccessor.getSessionAttributes().get("chatRooms");
...
}
설정 및 구성 (WebSocketConfig)
WebSocketConfig 클래스를 통해 구현한 인터셉터를 등록해줍니다! ⚙️
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.messaging.simp.config.ChannelRegistration;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
private final CustomHandshakeInterceptor customHandshakeInterceptor;
private final CustomChannelInterceptor customChannelInterceptor;
public WebSocketConfig(CustomHandshakeInterceptor customHandshakeInterceptor,
CustomChannelInterceptor customChannelInterceptor) {
this.customHandshakeInterceptor = customHandshakeInterceptor;
this.customChannelInterceptor = customChannelInterceptor;
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
// STOMP 엔드포인트 등록
registry.addEndpoint("/ws")
.setAllowedOrigins("*")
.withSockJS()
// 핸드셰이크 인터셉터 등록
.setInterceptors(customHandshakeInterceptor);
}
@Override
public void configureClientInboundChannel(ChannelRegistration registration) {
// 채널 인터셉터 등록
registration.interceptors(customChannelInterceptor);
}
}