Spring WebSocket - jak zabezpieczyć STOMP?

0

Próbuję napisać komunikację po WebSocket w Springu.

Na przykładzie czatu:

  • pokoje ogólnodostępne - tylko użytkownicy, którzy mają do nich dostęp, mogą otrzymywać wiadomości
  • komunikacja prywatna - tylko użytkownicy z danej konwersacji mogą otrzymywać wiadomości
  • będzie odpalonych wiele instancji serwera (wymagany mechanizm synchronizacji)

Spring mocno lobbuje za protokołem STOMP. Czy w tym przypadku to dobry pomysł? Mechanizm subskrypcji dużo rzeczy ułatwia, ale mam obawy o bezpieczeństwo, ponieważ każdy może zasubskrybować wszystko. Jak poprawnie zaimplementować kontrolę dostępu?

Cały ruch idzie przez proxy, który weryfikuje token JWT, a następnie dodaje nagłówek "User-Id". Po tym nagłówku aplikacja rozpoznaje zalogowanego użytkownika.

W przypadku WebSocketów trzeba odczytać ten nagłówek na etapie handshake i Spring nam to załatwia.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketConfigurer, WebSocketMessageBrokerConfigurer {
  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry
      .addHandler(new TextWebSocketHandler())
      .addInterceptors(new HttpSessionHandshakeInterceptor());  // ten interceptor kopiuje nagłówki HTTP do atrybutów WebSocket
  }
}

No ale jak załatwić kontrolę dostępu? Trzeba zweryfikować, czy użytkownik, co chce zasubskrybować /app/chat-room/name ma tam dostęp. Tu pojawia się kolejny problem. Teoretycznie użytkownicy mogą subskrybować /app/... ale to jest prefix do przechwytywania komunikatów przez serwer. Prefix dla użytkownika to /queue/user i tu jest kolejny problem, gdyż polega na Principal ze Spring Security (a i tak można zasubskrybować kolejkę dowolnego użytkownika).

To by pewnie wystarczyło, gdyż aplikacja wysyłałaby komunikaty do tych użytkowników, którzy mają dostęp do danych zasobów.

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketConfigurer, WebSocketMessageBrokerConfigurer {

  @Override
  public void configureMessageBroker(MessageBrokerRegistry config) {
    config
      .setApplicationDestinationPrefixes("/app")
      .setUserDestinationPrefix("/user")
      .enableSimpleBroker("/topic", "/queue");
  }

  @Override
  public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
    registry
      .addHandler(new TextWebSocketHandler())
      .addInterceptors(new HttpSessionHandshakeInterceptor())
      .setHandshakeHandler(handshakeHandler());   // tworzymy własny handler
  }

  @Bean
  public HandshakeHandler handshakeHandler() {
    return new WsHandshakeHandler();  // niestety Spring nie korzysta z tego handlera - dlaczego???
  }
}

public class WsHandshakeHandler extends DefaultHandshakeHandler {

  @Override
  public Principal determineUser(@NonNull ServerHttpRequest request,
                                 @NonNull WebSocketHandler wsHandler,
                                 @NonNull Map<String, Object> attributes) {
    var userIds = request.getHeaders().get("X-USER-ID");
    if (userIds != null && userIds.size() == 1) {
      return new WsPrincipal(UUID.fromString(userIds.get(0))); 
    }
    return null;
  }
}

Po stronie klienta jest biblioteka stompjs, która dba także po ponowne łączenie z serwerem i ponowne zasubskrybowanie istniejących subskrypcji.

Czy STOMP to dobry pomysł w tym przypadku?

0

WebSocketami bawiłem się dwa lata temu i nie pamiętam ich za bardzo, ale robiłem jakąś przykładową chat-appkę, więc może Ci się przyda: https://github.com/kkojot/multiroomchat-spring-vue
W SimpMessageHeaderAccessor możesz wcisnąć context usera i będzie Ci łatwiej odczytać go w controllerach, do pchania wiadomości na poszczególne endpointy możesz wykorzystać SimpMessageSendingOperations .

PS. z tego co widzę to zakomentowałem security w tym projekcie, także nie wiem na ile bezpiecznie jest trzymanie tego contextu usera w headerze

0

Tam chyba jest jakiś Handler na próbę połączenia, to tam może byś mógł wepchnąć uwierzytelnianie jakieś chyba StompSessionHandler

0

Jest w dokumentacji przykład z configureClientInboundChannel ale to wszystko toporne w implementacji. Pobawię się i dam znać, czy uda się wyciągnąć X-USER-ID, zabezpieczyć endpointy i wprowadzić bardziej szczegółową kontrolę dostępu. Wygląda na to, że korzystając z czystego WebSocket API w Springu lub JSR-356 (ze Springiem nie zadziała) programista ma większą kontrolę nad sesjami, kontrolą dostępu, itd.

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket-stomp-interceptors

STOMP jest wygodny ze względu na model subskrypcyjny i są biblioteki po stronie klienta (pod Angulara ng2-stompjs), które załatwiają także ponowne łączenie. Jednak analizując większe projekty nie znalazłem ani jednego, który by korzystał ze STOMP. Są to komunikaty JSON czasami oparte też na modelu subskrypcyjnym, choć w większości nie. Czy są jakieś inne popularne nakładki na WebSocket oprócz STOMP?

Druga kwestia to obsługa wielu serwerów - w przypadku STOMP zostaje nam chyba RabbitMQ, a co z innymi systemami typu Kafka czy Apache Pulsar?

1 użytkowników online, w tym zalogowanych: 0, gości: 1