야미의 개발

[Spring/SpringBoot] STOMP + SpringSecurity 에서 Principal이 null 일때 (WebSocketAuthInterceptor) 본문

스프링/웹 개발

[Spring/SpringBoot] STOMP + SpringSecurity 에서 Principal이 null 일때 (WebSocketAuthInterceptor)

채야미 2024. 12. 17. 13:55

최초 연결(CONNCECT)에서 인증을 하고 그 이후의 인증이 없으면 principal을 없다고 하는 문제인거같습니다.

따라서 아래의 코드처럼 

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

위의 코드를 추가해서 기존의 accessor가 존재하면 반환해 주는 것입니다.

 

 

사실 기본적인 코드는 아래와 같습니다.

StompHeaderAccessor accessor = StompHeaderAccessor.wrap(message);

 

이 코드는 모든 메시지에서 accessor를 새로 만들어서 주는 코드입니다.

하지만 제가 구현한 코드의 경우에는 연결뿐 아니라 연결 후에 메시지를 받아서 거기에 있는 principal로 userId를 세팅하기 때문에

연결시에 사용한 Principal을 계속해서 재사용 할 수 있는 코드가 적합합니다.

 

아래와 같이 userId를 Principal에서 받아서 사용하기 때문입니다.

@MessageMapping("/chat/rooms/{roomId}")
public void handleChatMessage(
    @DestinationVariable String roomId,
    @Payload SendMessageRequest payload,
    Principal principal
) {
    Long userId = Long.valueOf(principal.getName());
    ChatRoomMessageDTO newMessage = chatMessageService.sendMessage(
        new SendMessageCommand(
            Long.valueOf(roomId),
            new SenderCommand(userId, payload.getNickname()),
            payload.getMessage(),
            payload.getSentAt(),
            payload.getAttachmentRequests().stream().map(MessageContentCommand::from).toList()
        )
    );

    String topic = "/api/sub/chat/rooms/" + roomId; // 구독 경로 지정
    messagingTemplate.convertAndSend(topic, newMessage);
    System.out.println("메시지 발행됨: " + newMessage);
}

 

 

WebSocketAuthInterceptor.java

@RequiredArgsConstructor
@Component
public class WebSocketAuthInterceptor implements ChannelInterceptor {

    private final JwtUtil jwtUtil;
    private final UserDetailsService userDetailsService;

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {

        // 메시지 컨텍스트 유지
        StompHeaderAccessor accessor = MessageHeaderAccessor
                .getAccessor(message, StompHeaderAccessor.class);

        // WebSocket CONNECT 요청에서 Authorization 헤더 추출
        if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
            String token = accessor.getFirstNativeHeader("Authorization");
            
            // 여기서부터 엉망진창 JWT 인증 부분 시작
            if (token != null && token.startsWith("Bearer ")) {
                token = token.substring(7); // "Bearer " 제거

                if (jwtUtil.validateToken(token)) {
                    Long userId = jwtUtil.getSubject(token);

                    UserDetails userDetails = userDetailsService.loadUserByUsername(userId.toString());

                    Authentication authentication = new UsernamePasswordAuthenticationToken(
                            userDetails,
                            null,
                            userDetails.getAuthorities()
                    );

                    // SecurityContext에 Authentication 설정
                    SecurityContextHolder.getContext().setAuthentication(authentication);

                    // Principal 설정 추가
                    accessor.setUser(authentication);

                } else {
                    throw new TokenException("Invalid token");
                }
            } else {
                throw new TokenException("Missing Authorization header");
            }
        }

        return message;
    }
}

 

 

결론 : connect 시에만 principal을 설정하고 있다면 다른 SEND와 같은 작업에서는 principal은 null이다

 

1. 메시지를 보낼때에도 Principal을 사용하고 싶다면 매번 accessor를 거치게 하던지

2. 위의 코드에서처럼 설정을 사용해서 기존 context를 유지해서 Principal을 설정하자~~

3. 아니면 기본 SEND에서는 그냥 사용자 정보를 Message에서 받아서 저장하는 것도 방법

 

 

참고 블로그

https://velog.io/@ndynam99/STOMP-%EC%97%90%EC%84%9C-principal%EC%9D%B4-null-%EA%B0%92-%EB%9C%A8%EB%8A%94-%EA%B2%BD%EC%9A%B0

Comments