Utilisation de Spring Boot pour l'implémentation de WebSocket avec STOMP

Publié: 2022-03-11

Le protocole WebSocket est l'un des moyens permettant à votre application de gérer les messages en temps réel. Les alternatives les plus courantes sont les longues interrogations et les événements envoyés par le serveur. Chacune de ces solutions a ses avantages et ses inconvénients. Dans cet article, je vais vous montrer comment implémenter WebSockets avec Spring Boot Framework. Je couvrirai à la fois la configuration côté serveur et côté client, et nous utiliserons le protocole STOMP sur WebSocket pour communiquer les uns avec les autres.

Le côté serveur sera codé uniquement en Java. Mais, dans le cas du client, je montrerai des extraits écrits à la fois en Java et en JavaScript (SockJS) puisque, généralement, les clients WebSockets sont intégrés dans des applications frontales. Les exemples de code montreront comment diffuser des messages à plusieurs utilisateurs à l'aide du modèle pub-sub ainsi que comment envoyer des messages à un seul utilisateur. Dans une autre partie de l'article, je discuterai brièvement de la sécurisation de WebSockets et de la manière dont nous pouvons nous assurer que notre solution basée sur WebSocket restera opérationnelle même lorsque l'environnement ne prend pas en charge le protocole WebSocket.

Veuillez noter que le sujet de la sécurisation des WebSockets ne sera que brièvement abordé ici car il s'agit d'un sujet suffisamment complexe pour un article séparé. Pour cette raison, et plusieurs autres facteurs que j'aborde dans le WebSocket en production ? section à la fin, je recommande de faire des modifications avant d'utiliser cette configuration en production , lisez jusqu'à la fin pour une configuration prête pour la production avec des mesures de sécurité en place.

Protocoles WebSocket et STOMP

Le protocole WebSocket permet de mettre en place une communication bidirectionnelle entre applications. Il est important de savoir que HTTP n'est utilisé que pour la poignée de main initiale. Après cela, la connexion HTTP est mise à niveau vers une connexion TCP/IP nouvellement ouverte qui est utilisée par un WebSocket.

Le protocole WebSocket est un protocole plutôt de bas niveau. Il définit comment un flux d'octets est transformé en trames. Une trame peut contenir un texte ou un message binaire. Étant donné que le message lui-même ne fournit aucune information supplémentaire sur la manière de l'acheminer ou de le traiter, il est difficile d'implémenter des applications plus complexes sans écrire de code supplémentaire. Heureusement, la spécification WebSocket permet d'utiliser des sous-protocoles qui fonctionnent à un niveau d'application supérieur. L'un d'eux, pris en charge par Spring Framework, est STOMP.

STOMP est un protocole de messagerie texte simple qui a été initialement créé pour les langages de script tels que Ruby, Python et Perl pour se connecter aux courtiers de messages d'entreprise. Grâce à STOMP, les clients et les courtiers développés dans différentes langues peuvent s'envoyer et recevoir des messages entre eux. Le protocole WebSocket est parfois appelé TCP pour le Web. Analogiquement, STOMP est appelé HTTP pour le Web. Il définit une poignée de types de trames qui sont mappés sur des trames WebSockets, par exemple, CONNECT , SUBSCRIBE , UNSUBSCRIBE , ACK ou SEND . D'une part, ces commandes sont très pratiques pour gérer la communication tandis que, d'autre part, elles nous permettent d'implémenter des solutions avec des fonctionnalités plus sophistiquées comme l'acquittement des messages.

Côté serveur : Spring Boot et WebSockets

Pour construire le WebSocket côté serveur, nous utiliserons le framework Spring Boot qui accélère considérablement le développement d'applications autonomes et Web en Java. Spring Boot inclut le module spring-WebSocket , qui est compatible avec la norme Java WebSocket API (JSR-356).

L'implémentation de WebSocket côté serveur avec Spring Boot n'est pas une tâche très complexe et ne comprend que quelques étapes, que nous allons parcourir une par une.

Étape 1. Tout d'abord, nous devons ajouter la dépendance de la bibliothèque WebSocket.

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>

Si vous envisagez d'utiliser le format JSON pour les messages transmis, vous pouvez également inclure la dépendance GSON ou Jackson. Très probablement, vous aurez peut-être également besoin d'un cadre de sécurité, par exemple, Spring Security.

Étape 2. Ensuite, nous pouvons configurer Spring pour activer la messagerie WebSocket et STOMP.

 Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/mywebsockets") .setAllowedOrigins("mydomain.com").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config){ config.enableSimpleBroker("/topic/", "/queue/"); config.setApplicationDestinationPrefixes("/app"); } }

La méthode configureMessageBroker fait deux choses :

  1. Crée le courtier de messages en mémoire avec une ou plusieurs destinations pour l'envoi et la réception de messages. Dans l'exemple ci-dessus, deux préfixes de destination sont définis : topic et queue . Ils suivent la convention selon laquelle les destinations des messages à transmettre à tous les clients abonnés via le modèle pub-sub doivent être préfixées par topic . D'autre part, les destinations des messages privés sont généralement préfixées par queue .
  2. Définit l' app de préfixe utilisée pour filtrer les destinations gérées par des méthodes annotées avec @MessageMapping que vous implémenterez dans un contrôleur. Le contrôleur, après avoir traité le message, l'enverra au courtier.

Spring Boot WebSocket : comment les messages sont gérés côté serveur

Comment les messages sont gérés côté serveur (source : documentation Spring)


Pour en revenir à l'extrait ci-dessus - vous avez probablement remarqué un appel à la méthode withSockJS() - il active les options de secours SockJS. Pour faire court, cela laissera nos WebSockets fonctionner même si le protocole WebSocket n'est pas pris en charge par un navigateur Internet. J'aborderai ce sujet plus en détail un peu plus loin.

Il y a encore une chose qui doit être clarifiée : pourquoi nous appelons la méthode setAllowedOrigins() sur le point de terminaison. Il est souvent nécessaire car le comportement par défaut de WebSocket et SockJS est d'accepter uniquement les requêtes de même origine. Ainsi, si votre client et le côté serveur utilisent des domaines différents, cette méthode doit être appelée pour permettre la communication entre eux.

Étape 3 . Implémentez un contrôleur qui gérera les demandes des utilisateurs. Il diffusera le message reçu à tous les utilisateurs abonnés à un sujet donné.

Voici un exemple de méthode qui envoie des messages à la destination /topic/news .

 @MessageMapping("/news") @SendTo("/topic/news") public void broadcastNews(@Payload String message) { return message; }

Au lieu de l'annotation @SendTo , vous pouvez également utiliser SimpMessagingTemplate que vous pouvez autowire à l'intérieur de votre contrôleur.

 @MessageMapping("/news") public void broadcastNews(@Payload String message) { this.simpMessagingTemplate.convertAndSend("/topic/news", message) }

Dans les étapes ultérieures, vous souhaiterez peut-être ajouter des classes supplémentaires pour sécuriser vos points de terminaison, comme ResourceServerConfigurerAdapter ou WebSecurityConfigurerAdapter du framework Spring Security. En outre, il est souvent avantageux d'implémenter le modèle de message afin que le JSON transmis puisse être mappé sur des objets.

Construire le client WebSocket

La mise en œuvre d'un client est une tâche encore plus simple.

Étape 1. Client Autowire Spring STOMP.

 @Autowired private WebSocketStompClient stompClient;

Étape 2. Ouvrez une connexion.

 StompSessionHandler sessionHandler = new CustmStompSessionHandler(); StompSession stompSession = stompClient.connect(loggerServerQueueUrl, sessionHandler).get();

Une fois cela fait, il est possible d'envoyer un message à une destination. Le message sera envoyé à tous les utilisateurs abonnés à un sujet.

 stompSession.send("topic/greetings", "Hello new user");

Il est également possible de s'abonner aux messages.

 session.subscribe("topic/greetings", this); @Override public void handleFrame(StompHeaders headers, Object payload) { Message msg = (Message) payload; logger.info("Received : " + msg.getText()+ " from : " + msg.getFrom()); }

Parfois, il est nécessaire d'envoyer un message uniquement à un utilisateur dédié (par exemple lors de la mise en place d'un chat). Ensuite, le client et le côté serveur doivent utiliser une destination distincte dédiée à cette conversation privée. Le nom de la destination peut être créé en ajoutant un identifiant unique à un nom de destination général, par exemple, /queue/chat-user123 . Les identifiants de session HTTP ou STOMP peuvent être utilisés à cette fin.

Spring facilite grandement l'envoi de messages privés. Nous avons seulement besoin d'annoter la méthode d'un contrôleur avec @SendToUser . Ensuite, cette destination sera gérée par UserDestinationMessageHandler , qui s'appuie sur un identifiant de session. Côté client, lorsqu'un client s'abonne à une destination préfixée par /user , cette destination est transformée en une destination unique pour cet utilisateur. Côté serveur, une destination utilisateur est résolue en fonction du Principal .

Exemple de code côté serveur avec l'annotation @SendToUser :

 @MessageMapping("/greetings") @SendToUser("/queue/greetings") public String reply(@Payload String message, Principal user) { return "Hello " + message; }

Ou vous pouvez utiliser SimpMessagingTemplate :

 String username = ... this.simpMessagingTemplate.convertAndSendToUser(); username, "/queue/greetings", "Hello " + username);

Voyons maintenant comment implémenter un client JavaScript (SockJS) capable de recevoir des messages privés qui pourraient être envoyés par le code Java dans l'exemple ci-dessus. Il convient de savoir que les WebSockets font partie de la spécification HTML5 et sont pris en charge par la plupart des navigateurs modernes (Internet Explorer les prend en charge depuis la version 10).

 function connect() { var socket = new SockJS('/greetings'); stompClient = Stomp.over(socket); stompClient.connect({}, function (frame) { stompClient.subscribe('/user/queue/greetings', function (greeting) { showGreeting(JSON.parse(greeting.body).name); }); }); } function sendName() { stompClient.send("/app/greetings", {}, $("#name").val()); }

Comme vous l'avez probablement noté, pour recevoir des messages privés, le client doit s'abonner à une destination générale /queue/greetings préfixée par /user . Il n'a pas à s'embarrasser d'identifiants uniques. Cependant, le client doit d'abord se connecter à l'application, de sorte que l'objet Principal côté serveur est initialisé.

Sécuriser les WebSockets

De nombreuses applications Web utilisent une authentification basée sur les cookies. Par exemple, nous pouvons utiliser Spring Security pour restreindre l'accès à certaines pages ou contrôleurs uniquement aux utilisateurs connectés. Le contexte de sécurité de l'utilisateur est ensuite maintenu via une session HTTP basée sur des cookies qui est ensuite associée aux sessions WebSocket ou SockJS créées pour cet utilisateur. Les points de terminaison WebSockets peuvent être sécurisés comme toute autre demande, par exemple, dans WebSecurityConfigurerAdapter de Spring.

De nos jours, les applications Web utilisent souvent des API REST comme back-end et des jetons OAuth/JWT pour l'authentification et l'autorisation des utilisateurs. Le protocole WebSocket ne décrit pas comment les serveurs doivent authentifier les clients lors de la négociation HTTP. En pratique, les en-têtes HTTP standard (par exemple, Authorization) sont utilisés à cette fin. Malheureusement, il n'est pas pris en charge par tous les clients STOMP. Le client STOMP de Spring Java permet de définir des en-têtes pour la poignée de main :

 WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders(); handshakeHeaders.add(principalRequestHeader, principalRequestValue);

Mais le client JavaScript SockJS ne prend pas en charge l'envoi d'en-tête d'autorisation avec une requête SockJS. Cependant, il permet d'envoyer des paramètres de requête qui peuvent être utilisés pour transmettre un jeton. Cette approche nécessite d'écrire du code personnalisé côté serveur qui lira le jeton à partir des paramètres de requête et le validera. Il est également important de s'assurer que les jetons ne sont pas enregistrés avec les requêtes (ou que les journaux sont bien protégés) car cela pourrait introduire une grave violation de la sécurité.

Options de repli de SockJS

L'intégration avec WebSocket peut ne pas toujours se dérouler sans heurts. Certains navigateurs (par exemple, IE 9) ne prennent pas en charge les WebSockets. De plus, les proxys restrictifs peuvent rendre impossible la mise à niveau HTTP ou couper les connexions ouvertes trop longtemps. Dans de tels cas, SockJS vient à la rescousse.

Les transports SockJS se répartissent en trois catégories générales : WebSockets, HTTP Streaming et HTTP Long Polling. La communication commence par SockJS envoyant GET /info pour obtenir des informations de base du serveur. En fonction de la réponse, SockJS décide du transport à utiliser. Le premier choix est WebSockets. S'ils ne sont pas pris en charge, alors, si possible, le streaming est utilisé. Si cette option n'est pas non plus possible, alors l'interrogation est choisie comme méthode de transport.

WebSocket en production ?

Bien que cette configuration fonctionne, ce n'est pas la "meilleure". Spring Boot vous permet d'utiliser n'importe quel système de messagerie à part entière avec le protocole STOMP (par exemple, ActiveMQ, RabbitMQ), et un courtier externe peut prendre en charge plus d'opérations STOMP (par exemple, accuse réception, reçus) que le simple courtier que nous avons utilisé. STOMP Over WebSocket fournit des informations intéressantes sur WebSockets et le protocole STOMP. Il répertorie les systèmes de messagerie qui gèrent le protocole STOMP et pourraient constituer une meilleure solution à utiliser en production. Surtout si, en raison du nombre élevé de requêtes, le courtier de messages doit être mis en cluster. (Le courtier de messages simple de Spring n'est pas adapté au clustering.) Ensuite, au lieu d'activer le courtier simple dans WebSocketConfig , il est nécessaire d'activer le relais du courtier Stomp qui transfère les messages vers et depuis un courtier de messages externe. En résumé, un courtier de messages externe peut vous aider à créer une solution plus évolutive et plus robuste.

Si vous êtes prêt à poursuivre votre parcours de développeur Java en explorant Spring Boot, essayez de lire Utilisation de Spring Boot pour OAuth2 et JWT REST Protection ensuite.