본문 바로가기
코딩 공부/web & Spring

[Spring] STOMP + Spring Security에서 principal이 null 값 뜨는 경우

by 현장 2025. 12. 14.

STOMP + Spring Security에서 principal이 null 값 뜨는 경우

프로젝트를 진행하면서 웹소켓을 사용하면서 겪었던 문제와 그 해결 과정을 공유하고자 합니다. 제가 격은 문제는 채팅 기능에서 STOMP 연결 자체는 정상적으로 되지만, 전송 시점에 Authorization 헤더가 포함되지 않아 Principal이 null이 되는 문제입니다. 

 

StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

이 문제는 위의 코드를 아래처럼 수정하여 해결했습니다.

StompHeaderAccessor accessor = MessageHeaderAccessor
                .getAccessor(message, StompHeaderAccessor.class);

하지만 해결은 했으나 이유를 알아야 하기 때문에 무슨차인지 확인해봤습니다.

🏷️ 차이점

✅ wrap(message)

wrap()은 주어진 Message<?> 객체를 기반으로 새로운 접근자 객체, 즉 임시 복사본을 생성합니다. 이 방식은 특히 메시지 처리 파이프라인의 초기 단계인 ChannelInterceptor에서는 문제가 발생할 수 있습니다. Spring 시스템이 메시지 헤더에 중요한 정보(예: 유저의 STOMP 세션 ID)를 추가하며 계속 업데이트하고 있을 때, wrap()으로 만든 접근자는 그 최신 정보가 반영되지 않을 위험이 있습니다. 따라서 Principal 정보가 누락되거나 잘못 설정될 가능성이 높아져 불안정합니다.

✅ getAccessor(message, StompHeaderAccessor.class)

getAccessor()는 Spring 메시징 인프라 내에서 이미 처리되고 전달 중인 Message<?> 객체에서 특정 타입의 헤더 접근자를 안전하게 추출하는 공식적인 방법입니다. 이는 메시지 시스템이 '이 정보가 최종본이다'라고 인증한 공식 기록에 접근하는 것과 같습니다. getAccessor()를 사용하면, 메시지의 헤더와 관련된 모든 속성(Attributes)이 완전히 로드되고 준비된 상태에서 접근자를 가져올 수 있습니다.

🏷️ 결론

이러한 안정적인 접근 경로 덕분에, ChannelInterceptor가 Principal을 STOMP 세션에 바인딩할 때, Spring Security가 나중에 이 정보를 정확하게 읽어 @MessageMapping으로 성공적으로 주입할 수 있도록 모든 준비가 완료됩니다. 결국, Principal이 null이 되는 오류를 방지하는 가장 확실한 방법이 됩니다.

 ✔️ 수정 코드

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtChannelInterceptor implements ChannelInterceptor  {

    private final JwtTokenProvider jwtTokenProvider;
    private static final String AUTHORIZATION_HEADER = "Authorization";
    private static final String BEARER_PREFIX = "Bearer ";

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        // 헤더 가져옴
        StompHeaderAccessor accessor = MessageHeaderAccessor
                .getAccessor(message, StompHeaderAccessor.class);
        // 코멘트 가져오기
        StompCommand command = accessor.getCommand();

        // CONNECT 명령어 처리 (최초 연결 시 인증 시도)
        if (StompCommand.CONNECT.equals(command)) {
            String fullToken = accessor.getFirstNativeHeader(AUTHORIZATION_HEADER);

            if (fullToken == null || !fullToken.startsWith(BEARER_PREFIX)) {
                // 인증 실패 시, CONNECT 명령을 막기 위해 예외 발생
            }
            // Bearer 접두사 제거
            String token = fullToken.substring(BEARER_PREFIX.length());

            // 토큰 유효성 검증 및 인증 객체 설정
            // validateToken이 true를 반환해야 (토큰이 유효해야) 인증을 진행합니다.
            if (jwtTokenProvider.validateToken(token)) {
                try {
                    UsernamePasswordAuthenticationToken auth =
                            jwtTokenProvider.getAuthentication(token);
                    // Spring Security Context 설정
                    SecurityContextHolder.getContext().setAuthentication(auth);
                    // STOMP 세션에서도 인증 정보를 사용할 수 있도록 설정
                    accessor.setUser(auth);
                } catch (Exception e) {
                    // 유저 인증 과정 중 오류 발생 처리
                }
            } else {
                // JWT 토큰이 유효하지 않거나 만료 오류 처리
            }
        }

        return message;
    }
}

 

 

'코딩 공부 > web & Spring' 카테고리의 다른 글

[Web] Web Server와 WAS  (0) 2026.01.01
[Web / JWT / Spring] HttpOnly 쿠키와 보안  (0) 2025.12.21
[Spring / JWT] RefreshToken과 적용  (0) 2025.12.13
[Spring] Pageable  (0) 2025.04.08
[Spring] BCryptPasswordEncoder  (0) 2025.04.04