[실시간 채팅 구현기] Spring + WebSocket + Security 설정하기

Nicky·2024년 5월 13일
post-thumbnail

이제 본격적으로 WebSocket을 이용한 실시간 채팅을 구현해보자.

WebSocket 설정

종속성 추가

implementation 'org.springframework.boot:spring-boot-starter-websocket'

WebSocketConfig

@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    @Value("${react.url}")
    private String reactUrl;

    //  Simple Message Broker 활성화, Subscriber들에게 메시지를 전달
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOrigins(reactUrl)
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/subscribe");
        registry.setApplicationDestinationPrefixes("/publish");
    }
}

@EnableWebSocketMessageBroker

@EnableWebSocketMessageBroker는 Spring WebSocket 메시징 기능을 활성화하는 어노테이션으로 WebSocket 연결을 처리하고 STOMP 프로토콜을 사용할 수 있게 한다.

registerStompEndpoints()

"/ws" 경로를 WebSocket 연결을 위한 엔드포인트로 등록하고 setAllowedOrigins(reactUrl)를 통해 React 애플리케이션의 접근을 허용한다.

configureMessageBroker()

  • enableSimpleBroker("/subscribe"): 내장 Simple Message Broker를 활성화하고, "/subscribe" 주소로 메시지를 전송한다.
  • setApplicationDestinationPrefixes("/publish"): 클라이언트가 메시지를 보낼 때 "/publish" 주소를 사용하도록 설정한다.

메시지 흐름 과정

  1. 클라이언트는 "/publish" 주소로 메시지 전송
  2. Spring WebSocket 메시징 시스템이 이 메시지를 받아 내장 Simple Message Broker로 전달
  3. Simple Message Broker는 메시지를 "/subscribe" 주소로 구독 중인 클라이언트들에게 전송

보안 설정

현재 A1BnB는 JWT 토큰을 통해 인증된 사용자 정보를 조회하고 있다. 따라서 웹소켓 통신 시 사용자 정보를 받으려면 별도의 설정을 해줘야 한다.

종속성 추가

Spring Security Messaging는 WebSocket 및 STOMP 프로토콜을 사용하는 애플리케이션에 대한 보안 기능을 제공한다.

implementation 'org.springframework.security:spring-security-messaging'

WebSocketSecurityConfig

AbstractSecurityWebSocketMessageBrokerConfigurer를 상속받아 WebSocket 보안 설정을 구현할 수 있다.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry message) {
        message
                .nullDestMatcher().permitAll()
                .simpDestMatchers("/publish/**").authenticated()
                .simpSubscribeDestMatchers("/subscribe/**").authenticated()
                .anyMessage().denyAll();
    }

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }

}

configureInbound()

  • /publish/** 경로로 보내는 메시지는 인증된 사용자만 허용
  • /subscribe/** 경로로 구독하는 메시지는 인증된 사용자만 허용

WebSocketInterceptor

ChannelInterceptor 인터페이스를 구현하여 WebSocket 메시지 처리 과정을 커스터마이징할 수 있다.

@Component
@RequiredArgsConstructor
public class WebSocketInterceptor implements ChannelInterceptor {

    private final SecurityService securityService;

    @Override
    public Message<?> preSend(Message<?> message, MessageChannel channel) {
        StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);

        if (accessor.getCommand() == StompCommand.CONNECT) {
            String token = accessor.getFirstNativeHeader(HEADER_STRING.getValue()).replace(TOKEN_PREFIX.getValue(), "");
            // 토큰 검증
            securityService.validateToken(token);
            // 인증 정보 주입
            Authentication authentication = securityService.extractAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        return message;
    }

}

preSend()

커스텀한 preSend() 메서드는 다음과 같이 동작한다.

  1. WebSocket 연결 요청 수신받으면 preSend() 메서드가 호출
  2. STOMP 헤더에서 토큰을 추출
  3. 토큰 검증
  4. 검증 성공 시 토큰에서 인증 정보 추출
  5. 추출한 인증 정보를 SecurityContextHolder에 설정
  6. StompHeaderAccessor의 setUser() 메서드를 호출하여 인증 정보를 STOMP 헤더에 추가

WebSocketConfig

구현한 WebSocketInterceptorconfigureClientInboundChannel 메서드를 통해 등록할 수 있다.

@Configuration
@RequiredArgsConstructor
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {

    private final WebSocketInterceptor webSocketInterceptor;

    @Value("${react.url}")
    private String reactUrl;

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws")
                .setAllowedOrigins(reactUrl)
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/subscribe");
        registry.setApplicationDestinationPrefixes("/publish");
    }

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(webSocketInterceptor);
    }

}

인증 확인

브라우저의 콘솔 창을 확인해보면 다음과 같이 헤더에 토큰을 담고 연결 요청 시 사용자 정보를 불러오는 것을 확인할 수 있다.

profile
코딩 연구소

0개의 댓글