개발 낙서장

[TIL] 내일배움캠프 65일차 - STOMP JWT 인증 본문

Java/Sparta

[TIL] 내일배움캠프 65일차 - STOMP JWT 인증

권승준 2024. 3. 29. 22:31

 

 

오늘의 학습 키워드📚

STOMP에서 JWT 인증

기존에는 필터를 통해 JWT를 검증하고 Header와 SecurityContext에 추가해 Controller에서 @AuthenticationPrincipal 어노테이션으로 UserDetails 정보를 가져올 수 있었다.

    @MessageMapping("/chat-rooms/{roomId}/messages")
    @SendTo("/topic/chat-rooms/{roomId}")
    public GetMessageResponse sendMessage(
        @DestinationVariable Long roomId,
        CreateMessageRequest request,
        @AuthenticationPrincipal UserDetailsImpl userDetails
    ) throws Exception {
        Thread.sleep(500); // 지연 시뮬레이션

        return messageService.createMessage(roomId, request, userDetails.getUser());
    }

그래서 컨트롤러에서 인증된 유저 객체를 받아 활용하려 했지만 userDetails에 계속 null 값이 들어갔다.
채팅을 하려면 채팅을 작성한 사람이 누군지 알아야 하기에 채팅 테이블에는 유저에 대한 정보도 함께 들어갔다.
따라서 메세지를 보내는 요청에는 유저에 대한 정보가 필수적으로 들어가야 했다.

하지만 STOMP는 WebSocket 통신으로 HTTP 통신에서 사용하는 로직은 쓸 수 없었기에 당연히 Filter에서 수행되는 로직을 활용할 수 없어 추가적으로 구현해야 했다.

Interceptor

STOMP에도 HTTP의 Filter와 비슷한 기능을 하는 것이 있는데 바로 Interceptor이다.
말 그대로 필터처럼 요청이 도달하기 전에 가로채 특정 로직을 수행한 후에 진행하는 방식인데 Interceptor에서 유저 인증이나 사전 검증 작업 등을 많이 처리한다고 한다.

https://docs.spring.io/spring-framework/reference/web/websocket/stomp/authentication-token-based.html

 

Token Authentication :: Spring Framework

Spring Security OAuth provides support for token based security, including JSON Web Token (JWT). You can use this as the authentication mechanism in Web applications, including STOMP over WebSocket interactions, as described in the previous section (that i

docs.spring.io

스프링 공식 문서에 해당 부분에 대한 간단한 설명이 있으니 참고하면 좋을 것 같다.

기존에 만들었던 WebSocketConfig에서 configureClientInboundChannel 메소드를 오버라이드해 사용할 수 있다.
내가 원하는 건 'SEND' 커맨드로 요청이 들어왔을 때 JWT를 검증하고 헤더에 담아 보내는 작업이었다.

하지만 역시 HTTP Header는 사용할 수 없어서 수많은 구글링을 한 결과 StompHeaderAccessor 라는 클래스를 알게 되었다.
STOMP로 요청을 보낼 때 프레임에 헤더에 경로와 엔드포인트, 바디에 데이터 등을 담아서 보내는데 여기서 headers 라는 옵션으로 원하는 Key-Value로 헤더를 추가해 보낼 수 있다.
이 헤더는 NativeHeader로 들어가게 되며 StompHeaderAccessor에서 확인할 수 있다.

// 메세지 보내는 함수
function sendMessage() {
    const content = messageInput.val();
    stompClient.publish({
        destination: `/app/chat-rooms/${currentRoomId}/messages`,
        headers: {Authorization: getToken()},
        body: JSON.stringify({'content': content})
    });
    messageInput.val("");
}
    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor headerAccessor = MessageHeaderAccessor.getAccessor(message,
                    StompHeaderAccessor.class);

                if (headerAccessor != null && headerAccessor.getCommand() == StompCommand.SEND) {
                    String token = headerAccessor.getFirstNativeHeader("Authorization");

                    if (token != null) {
                        jwtUtil.validateToken(token);
                    }
                }

                return message;
            }
        });
    }

헤더 엑세서에서 'Authorization'으로 온 헤더가 있나 체크하고 토큰값 검증이 완료되면 정상적으로 해당 헤더를 사용할 수 있게 된다.

    @MessageMapping("/chat-rooms/{roomId}/messages")
    @SendTo("/topic/chat-rooms/{roomId}")
    public GetMessageResponse sendMessage(
        @DestinationVariable Long roomId,
        CreateMessageRequest request,
        @Header("Authorization") String token
    ) throws Exception {
        Thread.sleep(500); // 지연 시뮬레이션

        return messageService.createMessage(roomId, request, token);
    }

@Header 어노테이션을 통해 헤더 값을 가져와 해당 토큰 값을 통해 유저 정보를 추출해 사용하여 정상적으로 정보를 저장할 수 있었다.


오늘의 회고💬

웹 소켓... 너무 어려운 것 같다..... 실시간 채팅을 사용할 때는 아무 생각 없이 사용했는데 이걸 구현하려니까 진짜 어려운 것 같다
그래도 유저 정보를 받아 메세지를 주고 받는 것까진 완성을 해서 좀 더 다듬어서 프로젝트에 적용하면 될 것 같다!!!!

 

내일의 계획📜

주말 동안 채팅 기능을 좀 더 다듬어서 프로젝트에 적용할 예정이다.

Comments