Authenticated websockets in Spring Boot and ReactJS
Websockets are an easy way to update data on clients side without making request to server where there is no new data. It gives “wow effect” for clients and lower server costs for you.
Server side - Spring Framework
We will start from adding proper dependency in pom.xml
on backend side. In my case it the latest stable version was 2.0.2.RELEASE
.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
Basic websockets configuration in Spring is easy as copy-paste configuration files and handle connection on client side.
Create new configuration class annotated with @Configuration
and @EnableWebSocketMessageBroker
and extend it with
AbstractWebSocketMessageBrokerConfigurer
.
@Autowired
private ThreadPoolTaskScheduler threadPoolTaskScheduler;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config
.enableSimpleBroker("/queue")
.setTaskScheduler(threadPoolTaskScheduler);
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry
.addEndpoint("/ws")
.setAllowedOrigins(ALLOWED_ORIGINS)
.withSockJS()
.setTaskScheduler(threadPoolTaskScheduler);
}
Remember to provide TaskScheduler
which is required to sending messages.
In above example I also configured CORS by using list of allowed origins from *.yml
configuration.
Support for websocket authentication
Unfortunately as far as I know Spring websockets does not support authentication, so we need to implement it on our own.
I came up with very simple idea, I’m authenticating user on SessionSubscribeEvent
.
@EventListener(SessionSubscribeEvent.class)
public void onWebSocketSessionsConnected(SessionSubscribeEvent event) {
Message<byte[]> eventMessage = event.getMessage();
String token = getAuthorizationToken(eventMessage);
// Bearer xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// do whatever you need with user, throw exception if user should not be connected
// ...
}
private String getAuthorizationToken(Message<byte[]> message) {
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(message);
List<String> authorization = Optional.of(headerAccessor)
.map($ -> $.getNativeHeader(WebSocketHttpHeaders.AUTHORIZATION))
.orElse(Collections.emptyList());
// if header does not exists returns null instead empty list :/
return authorization.stream()
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Missing access token in Stomp message headers"));
}
Now we are ready to send data to connected clients! In my application I’m using application events to send updates with ease to connected companies from any place in code. In my case I’m sending update to dashboard page on every new transaction for company. Every user subscribe his company message channel and get update on every transaction or alert if occurred.
@Autowired
private SimpMessagingTemplate webSocket;
@EventListener(UpdateDashboardRequestEvent.class)
public void onClientUpdate(UpdateDashboardRequestEvent request) {
String companyName = request.getCompanyName();
log.info("Sending dashboard update to {}", companyName);
List<String> connectedCompanies = connectedUsersService.getConnectedCompanies();
// when user/companies successfully connects to server I add him to list of connected users/companies
boolean isConnected = connectedCompanies.contains(companyName);
if (!isConnected) {
log.warn("Company is not on connection list");
return;
}
String companyDestinationUrl = "/queue/" + updateForCompany + "/company";
Response response = ...
//response will be converted using message converters like on regular class annotated with @RestController
webSocket.convertAndSend(companyDestinationUrl, response);
}
Client side - ReactJS
On client side I’m using two additional dependencies, one for SockJS and second for webstomp of course.
sockjs-client - official SockJS client
webstomp-client - community developed webstomp client
import React from "react";
import SockJS from "sockjs-client";
import webstomp from "webstomp-client";
// types of Props & State
class Dashboard extends React.Component<Props, State> {
subscribeUpdates = () => {
const {companyName} = this.props;
this.topicSubscription = this.client.subscribe(
`/queue/${companyName}/company`, this.onUpdate,
{Authorization: `Bearer ${localStorage.getItem(ACCESS_TOKEN_KEY)}`},
);
};
connectSocket = () => {
const token = localStorage.getItem(ACCESS_TOKEN_KEY);
//pure accessToken without 'Bearer' part
const sockjs = new SockJS(
`${process.env.REACT_APP_API_URL}/ws`, null,
{ headers: {Authorization: `Bearer ${token}` }},
);
this.client = webstomp.over(sockjs, { debug: false });
this.client.connect({Authorization: `Bearer ${token}`}, this.subscribeUpdates);
};
componentDidMount() {
this.connectSocket();
}
onUpdate = ({body = "{}"}) => {
const message = JSON.parse(body)
// do whatever you want, eg. setState to update view
}
Conclusions
Done! 🌱 Below is an example how it looks like in my application.
In terminal, we see logs from the server for test demo company with a vending machines.
Every time machine sold a product data are sent to our server, and then transaction is
validated and eventually inserted to database. In the end I’m sending event
UpdateDashboardRequestEvent
to update the dashboard with WebSockets.