[프로젝트] 웹소켓으로 채팅 구현하기#3. SpringSecurity로 사용자 인증하기

bien·2024년 3월 14일
0

LegendsOfLeague

목록 보기
3/4
post-thumbnail

🚧 문제 상황

프로젝트에서 Spring Security를 활용하여 헤더로 JWT Token을 전달하는 방식을 사용해 로그인이 구현되었다. 하지만, WebSocket을 도입한 웹 채팅 기능에서, 정상적으로 소켓 연결이 이루어지지 않는 문제가 발생했다.

🔍 문제 상황 분석

WebSocket의 HTTP 요청 분석

WebSocekt은 한번의 HTTP 핸드셰이크 요청을 통해 TCP 연결을 수립한다. 이 점은 HTTP를 통한 초기 연결이 가능함을 의미하며, 따라서 Spring Security를 이용한 인증 과정 역시 용이하게 진행될 것이라고 예상했다. 그러나 JWT 토큰을 헤더에 담아 로그인 요청을 보내도, 서버측에서 정상적으로 처리되지 않는 문제가 발생했다.

공식문서 분석

공식 문서에서 확인 가능한 정보들은 다음과 같다.

  • WebSocket 프로토콜인 RFC 6455는 "WebSocket 핸드셰이크 중에 서버가 클라이언트를 인증할 수 있는 특정 방법을 규정하지 않는다"
  • 브라우저 클라이언트는 표준 인증 헤더(즉, 기본 HTTP 인증) 또는 쿠키만 사용할 수 있으며 사용자 정의 헤더는 제공하지 않는다. 마찬가지로 SockJS JavaScript 클라이언트는 SockJS 전송 요청과 함께 HTTP 헤더를 보내는 방법을 제공하지 않는다.
  • 대신 토큰을 쿼리 매개변수를 통한 토큰 사용을 허용하는데, 이는 서버 로그의 URL과 함께 기록될 수 있다는 단점이 있다.

WebSocket을 공부하면서 HTTP 핸드셰이크를 위한 request 속성값들도 확인했었다. 해당 request에 사용자 정의 헤더 Authorization: Bearer {token}을 추가하기만 하면 되는 간단한 문제라고 생각했는데, 문서에 따르면 WebSocket이 해당 기능을 제공하지 않고 있는 것 같았다..

공식문서에서 제시하는 다른 방법들은 다음과 같았다.

  • 방법1: (헤더 대신) 쿠키를 사용해 사용자 인증 정보를 전달한다.
  • 방법2: STOMP 메시징 프로토콜 수준 헤더를 사용해 인증한다.

💡 해결 전략: 쿠키 기반 인증으로 전환

프로젝트의 사용자 인증 방식이 쿠키 기반으로 변경되어, 자연스럽게 로그인 관련 문제가 해결되게 되었다! 따라서 편리하게 Controller에서 SpringSecurity에서 제공하는 사용자 정보를 사용할 수 있었다.

🛠️ 해결 방법: STOMP 헤더와 ChannelInterceptor 활용

따로 적용하진 않았지만, 공식문서 기반으로 간략하게 찾아본 "방법2: STOMP 수준 헤더 사용" 방법은 아래와 같다.

1. STOMP 헤더 이용한 사용자 검증

STOMP 클라이언트를 사용해 연결 시 인증 헤더를 전달한다.

const socket = new SockJS('/ws');
const stompClient = Stomp.over(socket);

const headers = {
  'Authorization': 'Bearer {token}'
};

stompClient.connect(headers, function(frame) {
  // 연결 성공 시의 콜백
});

2. ChannelInterceptor를 등록

@Configuration
@EnableWebSocketMessageBroker
public class MyConfig implements WebSocketMessageBrokerConfigurer {

	@Override
	public void configureClientInboundChannel(ChannelRegistration registration) {
		registration.interceptors(new ChannelInterceptor() {
			@Override
			public Message<?> preSend(Message<?> message, MessageChannel channel) {
				StompHeaderAccessor accessor =
					MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
				if (StompCommand.CONNECT.equals(accessor.getCommand())) {
					Authentication user = ... ; // access authentication header(s)
					accessor.setUser(user);
				}
				return message;
			}
		});
	}
}
  • CONNECT 메시지가 전송될때, 사용자 정보를 등록하고 사용하면 된다.
    • 나는 따로 코드를 구현하지 않았지만, 메서드 내부에 jwt 토큰을 검증하고 사용자 정보 조회 및 정보 설정의 코드를 추가해야 한다.
  • Spring이 인증된 사용자를 기록, 저장해 동일한 세션의 후속 STOMP 메시지와 연결한다.
  • ChannelInterceptor를 통해 구현한 사용자 정의 인증 인터셉터가 SpringSecurity의 인터셉터보다 먼저 정렬되어 있는지 확인해야 한다.
    • @Order(Ordered.HIGHEST_PRECEDENCE + 99)이 애노테이션을 추가하면 된다고 한다.

Reference

profile
Good Luck!

0개의 댓글