Implementing WebSocket with STOMP using Spring Boot

WebSockets are a popular method for enabling real-time communication in your applications, offering an alternative to techniques like long polling and server-sent events. This article will guide you through implementing WebSockets in a Spring Boot application, covering both server and client setup. We’ll use STOMP, a messaging protocol layered over WebSockets, for communication.

Our server-side code will be written in Java. For the client, we’ll use both Java and JavaScript (with SockJS) examples, as WebSocket clients are typically found in front-end applications. Our code will showcase broadcasting messages to multiple users (pub-sub model) and sending messages to individual users. We’ll also touch on securing WebSockets and ensuring our solution remains functional even without native WebSocket support.

It’s important to note that securing WebSockets is a complex topic and will only be briefly covered here. This, along with factors discussed in the WebSocket in Production? section, means I recommend making modifications before using this setup in production. Read on for a production-ready approach with enhanced security.

WebSocket and STOMP Protocols

The WebSocket protocol allows two applications to communicate in both directions. It starts with a standard HTTP handshake. Once that’s complete, the connection upgrades to a TCP/IP connection, which the WebSocket then uses.

As a low-level protocol, WebSocket defines how bytes are converted into frames, which can carry text or binary messages. Without extra information on routing or processing, building complex applications solely with WebSockets is difficult. That’s why the specification allows for sub-protocols that operate at a higher level. One such sub-protocol supported by Spring is STOMP.

STOMP is a text-based messaging protocol initially designed to connect scripting languages like Ruby, Python, and Perl to enterprise message brokers. It enables cross-language communication for sending and receiving messages. The analogy of WebSocket being “TCP for the Web” extends to STOMP being “HTTP for the Web.” STOMP defines various frame types, such as CONNECT, SUBSCRIBE, UNSUBSCRIBE, ACK, and SEND, mapped onto WebSocket frames. These commands simplify communication management and enable features like message acknowledgment.

The Server-side: Spring Boot and WebSockets

Leveraging the Spring Boot framework, we can rapidly develop the WebSocket server-side of our application. Spring Boot’s spring-WebSocket module offers compatibility with the Java WebSocket API standard (JSR-356).

Let’s break down the implementation into manageable steps:

Step 1. Begin by adding the WebSocket library dependency:

1
2
3
4
<dependency>
  <groupId>org.springframework.boot</groupId>            
  <artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

For transmitting messages in JSON format, include the GSON or Jackson dependency. Additionally, a security framework like Spring Security might be necessary.

Step 2. Configure Spring to enable WebSocket and STOMP messaging.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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");
  }
}

The configureMessageBroker method accomplishes two tasks:

  1. It establishes an in-memory message broker with destinations for sending and receiving messages. In this example, topic and queue are used as prefixes. By convention, destinations for broadcasting messages to all subscribed clients (pub-sub) are prefixed with topic. Destinations for private messages use the queue prefix.
  2. It defines the app prefix for filtering destinations handled by controller methods annotated with @MessageMapping. The controller processes incoming messages before sending them to the broker.
Spring Boot WebSocket: How messages are handled on server-side
How messages are handled on server-side (source: Spring documentation)

The withSockJS() call enables SockJS fallback options, ensuring WebSockets function even without native browser support. We’ll delve into this further later.

The setAllowedOrigins() method on the endpoint is crucial when the client and server reside on different domains. By default, WebSocket and SockJS only allow same-origin requests, so this method enables cross-domain communication.

Step 3. Create a controller to handle user requests and broadcast received messages to all subscribed users.

This example demonstrates a method that sends messages to the /topic/news destination:

1
2
3
4
5
@MessageMapping("/news")
@SendTo("/topic/news")
public void broadcastNews(@Payload String message) {
  return message;
}

As an alternative to @SendTo, autowire SimpMessagingTemplate inside your controller.

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

To enhance security, consider adding classes like ResourceServerConfigurerAdapter or WebSecurityConfigurerAdapter from the Spring Security framework in later steps. Implementing a message model for mapping transmitted JSON to objects can also be beneficial.

Building the WebSocket Client

Creating the client is even more straightforward.

Step 1. Autowire the Spring STOMP client.

1
2
@Autowired
private WebSocketStompClient stompClient;

Step 2. Open a connection.

1
2
3
4
StompSessionHandler sessionHandler = new CustmStompSessionHandler();

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

Now we can send messages to a specific destination, reaching all subscribed users.

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

Subscribing to messages is just as easy.

1
2
3
4
5
6
7
8
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());
}

Sometimes, sending messages to a specific user (like in a chat application) requires a dedicated destination. This private conversation destination can be created by appending a unique identifier, such as /queue/chat-user123, to a general destination name. HTTP session or STOMP session identifiers are suitable for this purpose.

Spring simplifies this process with the @SendToUser annotation. When a controller method uses this annotation, UserDestinationMessageHandler handles the destination based on the session identifier. Client-side subscriptions to destinations prefixed with /user are transformed into user-specific destinations. On the server, user destinations are resolved using the user’s Principal object.

Here’s an example of using @SendToUser on the server:

1
2
3
4
5
6
@MessageMapping("/greetings")
@SendToUser("/queue/greetings")
public String reply(@Payload String message,
   Principal user) {
 return  "Hello " + message;
}

You can achieve the same result with SimpMessagingTemplate.

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

Let’s explore a JavaScript (SockJS) client that receives private messages from the Java code above. Modern browsers (Internet Explorer 10 and later) support WebSockets as part of the HTML5 specification.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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());
}

To receive private messages, the client subscribes to the general destination /queue/greetings prefixed with /user. No unique identifiers are needed. However, the client must log in to the application beforehand, initializing the Principal object on the server.

Securing WebSockets

Cookie-based authentication is common in web applications. For example, Spring Security can restrict access to specific pages or controllers based on user login status. The user’s security context is maintained through a cookie-based HTTP session, subsequently associated with the user’s WebSocket or SockJS sessions. Securing WebSockets endpoints can be achieved similarly to other requests, like using Spring’s WebSecurityConfigurerAdapter.

Modern web applications often utilize REST APIs with OAuth/JWT tokens for authentication and authorization. While the WebSocket protocol doesn’t explicitly define client authentication during the HTTP handshake, standard HTTP headers like “Authorization” are commonly used. Unfortunately, not all STOMP clients support this. Spring’s Java STOMP client allows setting headers for the handshake:

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

However, the SockJS JavaScript client doesn’t support sending authorization headers. As a workaround, you can send query parameters containing the token, which requires custom server-side code to read, validate, and prevent tokens from being logged alongside requests (a potential security risk).

SockJS Fallback Options

Integrating WebSockets can be tricky. Older browsers (like IE 9) lack support, and restrictive proxies might prevent the HTTP upgrade or sever long-lived connections. SockJS addresses these challenges with fallback options.

SockJS transports are categorized into WebSockets, HTTP Streaming, and HTTP Long Polling. It starts by sending a GET /info request to gather server information. Based on the response, it chooses the appropriate transport: WebSockets first, then Streaming if possible, and finally Polling as the last resort.

WebSocket in Production?

While functional, this setup isn’t ideal for production. Spring Boot supports integrating full-fledged messaging systems like ActiveMQ or RabbitMQ that handle the STOMP protocol. These external brokers often support more STOMP operations (e.g., acknowledgments, receipts) than the simple broker we used. STOMP Over WebSocket offers valuable insights into WebSockets and STOMP, listing messaging systems that could be better suited for production environments.

Consider an external message broker if your application demands high scalability and robustness, especially when clustering the message broker becomes necessary (Spring’s simple broker doesn’t support clustering). In such cases, instead of enabling the simple broker in WebSocketConfig, enable the Stomp broker relay to forward messages to and from the external broker.

To further your Java development journey with Spring Boot, explore Using Spring Boot for OAuth2 and JWT REST Protection next.

Licensed under CC BY-NC-SA 4.0