트러블슈팅

STOMP 메시지 전송 시 security context holder에서 유저 정보를 가져올 수 없는 error

계양 꿀주먹 2024. 8. 16. 17:48

문제

Jul 24 17:13:51 ip-172-31-44-203 web[1195047]: ----------------- stomp command : SEND
Jul 24 17:13:51 ip-172-31-44-203 web[1195047]: ----------------- authentication : kimtahwn@naver.com
Jul 24 17:13:51 ip-172-31-44-203 web[1195047]: 2024-07-24T17:13:51.882+09:00 ERROR 1195047 --- [nboundChannel-6] .WebSocketAnnotationMethodMessageHandler : Unhandled exception from message handler method
Jul 24 17:13:51 ip-172-31-44-203 web[1195047]: java.lang.NullPointerException: Cannot invoke "org.springframework.security.core.Authentication.getPrincipal()" because "authentication" is null
Jul 24 17:13:51 ip-172-31-44-203 web[1195047]: #011at com.example.CatchStudy.service.UsersService.getEmail(UsersService.java:75) ~[!/:0.0.1-SNAPSHOT]
Jul 24 17:13:51 ip-172-31-44-203 web[1195047]: #011at com.example.CatchStudy.service.UsersService.getCurrentUserId(UsersService.java:55) ~[!/:0.0.1-SNAPSHOT]

Stomp를 사용하여 메시지를 전송할 때, channelInterCeptor에서 securityContextHolder에 사용자의 정보가 저장되는 것을 확인했지만 메시지를 저장하는 메소드가 호출될 때(Controller, Service)는 securityContextHolder에서 사용자의 정보를 가져오지 못 해 에러가 발생한다.


원인

SecurityContext의 전파: SecurityContextHolder는 스레드 로컬(Thread Local) 저장소를 사용하여 현재 스레드에 대한 보안 컨텍스트를 저장한다.

send 명령을 처리하는 Interceptor 핸들러와 controller가 서로 다른 스레드에서 실행되어, SecurityContext가 전파되지 않아 null이 발생한다.

 

 

  • StompHandler (ChannelInterceptor):
    • StompHandler는 WebSocket 통신에서 채널을 가로채는 인터셉터 역할 -> 메시지 전송 전에 스레드 실행
    • 이 클래스의 preSend 메소드는 STOMP 메시지가 채널을 통해 전송되기 전에 실행이 단계에서 주로 보안 및 인증 관련 처리를 하며, 메시지의 헤더를 분석하고 인증 토큰을 검증하여 Spring Security Context에 사용자 정보를 설정
    • Spring에서는 WebSocket 메시지 처리를 위해 내부적으로 스레드 풀을 사용, preSend 메소드는 스레드 풀 내의 스레드에서 실행
  • MessageController (메시지 처리 컨트롤러):
    • MessageController 클래스의 메소드(createMessage)는 클라이언트가 메시지를 전송했을 때 실행.
    • 클라이언트가 /chatRoomId/chat로 메시지를 전송하면 이 메소드가 호출되어 메시지를 처리하고 응답을 생성
    • 메시지 컨트롤러 메소드는 WebSocket 메시지 처리 스레드 풀에서 실행되고 Spring은 각 메시지에 대해 적절한 스레드를 할당하여 메시지를 처리

 

** Spring에서 HTTP 와 WebSocket의 차이

- HTTP :

  동기 방식이며 각 요청마다 thread 할당

 

- WebSocket :

  비동기 방식이며 지속적 연결을 통해 실시간 통신을 지원

  핸드셰이크를 통해 연결이 설정되면 spring에서는 비동기적으로 메시지를 처리하기 위해 스레드를 사용하며 연결이 해제되면 스레드 해제


해결

  • 메시지를 전송할 때, front에게 header에 jwtToken을 전달받아 해결
@MessageMapping("/{chatRoomId}/chat")
@SendTo("/sub/{chatRoomId}/chat")
public void createMessage(@DestinationVariable long chatRoomId, MessageRequestDto messageRequestDto,
                          @Header("Authorization") String jwtToken) {
    ...
}