-
Notifications
You must be signed in to change notification settings - Fork 0
Websocket Session Handle
Mambo edited this page Apr 20, 2026
·
5 revisions
- WebSocketMessageBrokerConfigurer.configureWebSocketTransport -> WebSocketTransportRegistration.addDecoratorFactory -> WebSocketHandlerDecoratorFactory.decorator
- SubProtocolWebSocketHandler X
package com.xxx.websocket.interceptor;
import com.xxx.bean.User;
import com.xxx.service.JwtTokenService;
import com.xxx.websocket.bean.UserPrincipal;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageDeliveryException;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* WebSocket 연결 시 JWT 토큰을 검증하고 사용자 정보를 Principal로 주입하는 인터셉터
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class WebSocketAuthInterceptor implements ChannelInterceptor {
private final JwtTokenService jwtTokenService;
private static final String HEADER_AUTH = "Authorization";
private static final String TOKEN_PREFIX = "Bearer ";
@Override
@SuppressWarnings("unchecked")
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
Map<String, List<String>> nativeHeaders = (Map<String, List<String>>) message.getHeaders().get("nativeHeaders");
try {
String token = resolveToken(nativeHeaders)
.orElseThrow(() -> new MessageDeliveryException("Authentication failed. Invalid token."));
User user = jwtTokenService.getUser(token);
if (user == null) {
throw new MessageDeliveryException("Authentication failed. User account not found.");
}
log.info("[WebSocket] CONNECT - session: {}, user: {}", accessor.getSessionId(), user.getId());
accessor.setUser(new UserPrincipal(user));
} catch (Exception e) {
log.warn("[WebSocket] Authentication failed in CONNECT: {}", e.getMessage());
}
}
return message;
}
/**
* 헤더에서 Bearer 토큰을 추출합니다.
*
* @param headers Native 헤더 맵
* @return 추출된 토큰 (Optional)
*/
private Optional<String> resolveToken(Map<String, List<String>> headers) {
return Optional.ofNullable(headers)
.map(h -> h.get(HEADER_AUTH))
.filter(auth -> !auth.isEmpty())
.map(auth -> auth.get(0))
.map(token -> StringUtils.removeStart(token, TOKEN_PREFIX))
.map(token -> StringUtils.stripEnd(token, ";"))
.filter(token -> StringUtils.isNotBlank(token) && !"null".equalsIgnoreCase(token));
}
}- StompEndpointRegistry 에 등록하는 HandshakeInterceptor 는 쿠키로 보내야만 인증이 가능
- ChannelRegistration 에 등록하는 ChannelInterceptor 는 STOMP의 CONNECT 프레임이 전송되는 시점에 동작
- 클라이언에서 STOMP 헤더로 토큰을 보내고 있으므로 HandshakeInterceptor 보다는 ChannelInterceptor 가 적합
| 단계 | 발생 이벤트 | 관련 클래스 / 메서드 | 주요 역할 |
|---|---|---|---|
| 1단계 | HTTPHandshake | StompEndpointRegistry | 클라이언트가 ws-stomp 경로로 연결 요청. HTTP를 WebSocket으로 업그레이드. |
| 2단계 | Raw Socket Open | WebSocketHandlerDecorator | 로우 레벨의 소켓이 물리적으로 연결됨. afterConnectionEstablished가 실행되며 메모리 저장소에 세션 ID 등록. |
| 3단계 | STOMP CONNECT | WebSocketAuthInterceptor | 클라이언트가 내보낸 **STOMP 프레임(헤더에 토큰 포함)**이 도착. 여기서 토큰을 검증하고 성공 시 Principal을 주입. |
| 4단계 | Connection Event | WebSocketEventListener | 스프링 애플리케이션 내부에 SessionConnectEvent 발생. 주입된 Principal 정보를 기반으로 사용자별 세션 맵핑 처리. |
| 단계 | 발생 이벤트 | 관련 클래스 / 메서드 | 주요 역할 |
|---|---|---|---|
| 1단계 | STOMP DISCONNECT | WebSocketEventListener | 클라이언트가 정상적으로 종료 요청을 보냄. SessionDisconnectEvent 수신 및 관련 리소스 정리. |
| 2단계 | Raw Socket Close | WebSocketHandlerDecorator | 소켓이 물리적으로 닫힘. afterConnectionClosed가 실행되며 메모리 저장소에서 세션 삭제. |
| 강제종료 | Manual Close | WebSocketSessionRepository | 서버 측에서 토큰 만료 등으로 세션을 찾아 session.close() 호출 시 즉시 2단계로 이동. |