Spring WebSocket With STOMP
Spring WebSocket With STOMP
WebSocket
Implementation with
STOMP
Table of Contents
Using Spring Boot for WebSocket Implementation with STOMP............................1
WebSocket and STOMP Protocols.............................................................................................2
The Server-side: Spring Boot and WebSockets.......................................................................3
Building the WebSocket Client....................................................................................................5
Securing WebSockets...................................................................................................................8
SockJS Fallback Options.............................................................................................................9
WebSocket in Production?..........................................................................................................9
UNDERSTANDING THE BASICS...............................................................................................10
What is STOMP?....................................................................................................................................10
What are WebSockets used for?....................................................................................................10
How does a WebSocket work?........................................................................................................10
What is Spring Boot and why it is used?....................................................................................10
The WebSocket protocol is one of the ways to make your application handle real-time
messages. The most common alternatives are long polling and server-sent events.
Each of these solutions has its advantages and drawbacks. In this article, I am going
to show you how to implement WebSockets with the Spring Boot Framework. I will
cover both the server-side and the client-side setup, and we will use STOMP over
WebSocket protocol to communicate with each other.
The server-side will be coded purely in Java. But, in the case of the client, I will show
snippets written both in Java and in JavaScript (SockJS) since, typically, WebSockets
clients are embedded in front-end applications. The code examples will demonstrate
how to broadcast messages to multiple users using the pub-sub model as well as how
to send messages only to a single user. In a further part of the article, I will briefly
discuss securing WebSockets and how we can ensure that our WebSocket-based
solution will stay operational even when the environment does not support the
WebSocket protocol.
Please note that the topic of securing WebSockets will only briefly be touched on here
since it is a complex enough topic for a separate article. Due to this, and several other
factors that I touch on in the WebSocket in Production? section in the end, I
recommend making modifications before using this setup in production,
read until the end for a production-ready setup with security measures in place.
STOMP is a simple text-based messaging protocol that was initially created for
scripting languages such as Ruby, Python, and Perl to connect to enterprise message
brokers. Thanks to STOMP, clients and brokers developed in different languages can
send and receive messages to and from each other. The WebSocket protocol is
sometimes called TCP for Web. Analogically, STOMP is called HTTP for Web . It
defines a handful of frame types that are mapped onto WebSockets frames,
e.g., CONNECT , SUBSCRIBE , UNSUBSCRIBE , ACK , or SEND . On one hand, these
commands are very handy to manage communication while, on the other, they allow
us to implement solutions with more sophisticated features like message
acknowledgment.
To build the WebSocket server-side, we will utilize the Spring Boot framework which
significantly speeds up the development of standalone and web applications in Java.
Spring Boot includes the spring-WebSocket module, which is compatible with the
If you plan to use JSON format for transmitted messages, you may want to include
also the GSON or Jackson dependency. Quite likely, you may additionally need a
security framework, for instance, Spring Security.
Step 2. Then, we can configure Spring to enable WebSocket and STOMP messaging.
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" );
}
}
that destinations for messages to be carried on to all subscribed clients via the
pub-sub model should be prefixed with topic . On the other hand,
destinations for private messages are typically prefixed by queue .
2. Defines the prefix app that is used to filter destinations handled by methods
annotated with @MessageMapping which you will implement in a controller.
The controller, after processing the message, will send it to the broker.
will let our WebSockets work even if the WebSocket protocol is not supported by an
internet browser. I will discuss this topic in greater detail a bit further.
There is one more thing that needs clarifying—why we
call setAllowedOrigins() method on the endpoint. It is often required because the
default behavior of WebSocket and SockJS is to accept only same-origin requests. So,
if your client and the server-side use different domains, this method needs to be
called to allow the communication between them.
Step 3. Implement a controller that will handle user requests. It will broadcast
received message to all users subscribed to a given topic.
Here is a sample method that sends messages to the destination /topic/news .
@MessageMapping ( "/news" )
@SendTo ( "/topic/news" )
public void broadcastNews(@Payload String message) {
return message;
}
from the Spring Security framework. Also, it is often beneficial to implement the
message model so that transmitted JSON can be mapped to objects.
@Autowired
private WebSocketStompClient stompClient;
Once this is done, it is possible to send a message to a destination. The message will
be sent to all users subscribed to a topic.
@Override
public void handleFrame(StompHeaders headers, Object payload) {
Message msg = (Message) payload;
logger.info( "Received : " + msg.getText()+ " from : " +
msg.getFrom());
}
Sometimes it is needed to send a message only to a dedicated user (for instance when
implementing a chat). Then, the client and the server-side must use a separate
destination dedicated to this private conversation. The name of the destination may
be created by appending a unique identifier to a general destination name,
e.g., /queue/chat-user123 . HTTP Session or STOMP session identifiers can be
destination is transformed into a destination unique for this user. On the server-side,
a user destination is resolved based on a user’s Principal .
Sample server-side code with @SendToUser annotation:
@MessageMapping ( "/greetings" )
@SendToUser ( "/queue/greetings" )
public String reply(@Payload String message,
Principal user) {
return "Hello " + message;
}
Let’s now look at how to implement a JavaScript (SockJS) client capable of receiving
private messages which could be sent by the Java code in the example above. It is
worth knowing that WebSockets are a part of HTML5 specification and are
supported by most modern browsers (Internet Explorer supports them since 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());
}
As you have probably noted, to receive private messages, the client needs to
subscribe to a general destination /queue/greetings prefixed with /user . It does
not have to bother with any unique identifiers. However, the client needs to login to
the application before, so the Principal object on the server-side is initialized.
Securing WebSockets
Many web applications use cookie-based authentication. For instance, we can use
Spring Security to restrict access to certain pages or Controllers only to logged users.
User security context is then maintained through cookie-based HTTP session that is
later associated with WebSocket or SockJS sessions created for that user.
WebSockets endpoints can be secured as any other requests, e.g., in
Spring’s WebSecurityConfigurerAdapter .
Nowadays, web applications often use REST APIs as their back-end and OAuth/JWT
tokens for user authentication and authorization. The WebSocket protocol does not
describe how servers should authenticate clients during HTTP handshake. In
practice, standard HTTP headers (e.g., Authorization) are used for this purpose.
Unfortunately, it is not supported by all STOMP clients. Spring Java’s STOMP client
allows to set headers for the handshake:
WebSocketHttpHeaders handshakeHeaders = new WebSocketHttpHeaders();
handshakeHeaders.add(principalRequestHeader, principalRequestValue);
But the SockJS JavaScript client does not support sending authorization header with
a SockJS request. However, it allows for sending query parameters that can be used
to pass a token. This approach requires writing custom code in the server-side that
will read the token from the query parameters and validate it. It is also important to
make sure that tokens are not logged together with requests (or logs are well
protected) since this could introduce a serious security violation.
Integration with WebSocket may not always go smoothly. Some browsers (e.g., IE 9)
do not support WebSockets. What is more, restrictive proxies may make it
impossible to perform the HTTP upgrade or they cut connections that are open for
too long. In such cases, SockJS comes to the rescue.
SockJS transports fall in three general categories: WebSockets, HTTP Streaming, and
HTTP Long Polling. The communication starts with SockJS sending GET /info to
obtain basic information from the server. Basing on the response, SockJS decides on
the transport to be used. The first choice are WebSockets. If they are not supported,
then, if possible, Streaming is used. If this option is also not possible, then Polling is
chosen as a transport method.
WebSocket in Production?
While this setup works, it isn’t the “best.” Spring Boot allows you to use any full-
fledged messaging system with the STOMP protocol (e.g., ActiveMQ, RabbitMQ),
and an external broker may support more STOMP operations (e.g., acknowledges,
receipts) than the simple broker we used. STOMP Over WebSocket provides
interesting information about WebSockets and STOMP protocol. It lists messaging
systems that handle STOMP protocol and could be a better solution to use in
production. Especially if, due to the high number of requests, the message broker
needs to be clustered. (Spring’s simple message broker is not suitable for clustering.)
Then, instead of enabling the simple broker in WebSocketConfig , it is required to
enable the Stomp broker relay that forwards messages to and from an external
message broker. To sum up, an external message broker may help you build a more
scalable and robust solution.
If you’re ready to continue your Java Developer journey exploring Spring Boot, try
reading Using Spring Boot for OAuth2 and JWT REST Protection next.
What is STOMP?
STOMP is the Simple (or Streaming) Text Oriented Messaging Protocol. It uses a set
of commands like CONNECT, SEND, or SUBSCRIBE to manage the conversation.
STOMP clients, written in any language, can talk with any message broker
supporting the protocol.
WebSockets are typically used to make web applications more interactive. They can
be helpful when implementing social feeds, online chats, news updates, or location-
based apps.
Tomasz is a creative developer with over ten years of experience at designing and implementing
Java applications. He is a team player, enthusiastic about learning new technologies and trying
out different ideas and approaches to process improvement. Tomasz has worked for
international companies (Hewlett-Packard) as well as for Silicon Valley startups. Currently he
helps small and medium companies to create Java-based solutions.
The containing structure can be JSON Web Signature (JWS) or JSON Web
Encryption (JWE).
JWT can be chosen as the format for access and refresh tokens used inside the
OAuth2 protocol.
OAuth2 and JWT gained a huge popularity over the last years because of the
following features:
Fixed lifetime for token add additional complexity for managing long-
running sessions without compromising security (e.g. refresh token)
First of all, a brief introduction to the technology stack selected for this project.
The project management tool of choice is Maven, but due to the project’s simplicity,
it should not be difficult to switch to other tools like Gradle.
In the article’s continuation, we focus on Spring Security aspects only, but all code
excerpts are taken from a fully working server-side application which source code is
available in a public repository along with a client consuming its REST resources.
Let’s take a quick look at Spring Security architecture (a more detailed guide can be
found here).
Security is mostly about authentication, i.e. the verification of the identity,
and authorization, the grant of access rights to resources.
Spring security supports a huge range of authentication models, either provided by
third parties or implemented natively. A list can be found here.
Regarding authorization, three main areas are identified:
Authentication
Authorization
The main interface is AccessDecisionManager ; which implementations for all three
areas listed above delegate to a chain of AccessDecisionVoter . Each instance of the
latter interface represents an association between an Authentication (a user
identity, named principal), a resource and a collection of ConfigAttribute , the set
of rules which describes how the resource’s owner allowed the access to the resource
itself, maybe through the use of user roles.
The security for a web application is implemented using the basic elements described
above in a chain of servlet filters, and the class WebSecurityConfigurerAdapter is
While multiple server-side OAuth2 libraries exist in the Java world (a list can be
found here), the spring-based implementation is the natural choice as we expect to
find it well integrated into Spring Security architecture and therefore avoid the need
to handle much of the low-level details for its use.
All security-related library dependencies are handled by Maven with the help of
Spring Boot, which is the only component requiring an explicit version inside
maven’s configuration file pom.xml (i.e. library versions are automatically inferred
by Maven choosing the most up-to-date version compatible with the inserted Spring
Boot version).
Find below the excerpt from maven’s configuration file pom.xml containing the
dependencies related to Spring Boot security:
<dependency>
<groupId> org.springframework.boot </groupId>
<artifactId> spring-boot-starter-security </artifactId>
</dependency>
<dependency>
<groupId> org.springframework.security.oauth.boot </groupId>
<artifactId> spring-security-oauth2-autoconfigure </artifactId>
<version> 2.1.0.RELEASE </version>
</dependency>
The app acts both as OAuth2 authorization server/resource owner and as resource
server.
The protected resources (as resource server) are published under /api/ path, while
authentication path (as resource owner/authorization server) is mapped
to /oauth/token, following proposed default.
App’s structure:
extending AuthorizationServerConfigurerAdapter
ResourceServerConfiguration ,
extending ResourceServerConfigurerAdapter
ServerSecurityConfig , extending WebSecurityConfigurerAdapter
one related to the resource owner behavior and both are contained in the
class AuthorizationServerConfigurerAdapter .
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.authentication.AuthenticationManager;
import
org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import
org.springframework.security.oauth2.config.annotation.configurers.Clie
ntDetailsServiceConfigurer;
import
org.springframework.security.oauth2.config.annotation.web.configuratio
n.AuthorizationServerConfigurerAdapter;
import
org.springframework.security.oauth2.config.annotation.web.configuratio
n.EnableAuthorizationServer;
import
org.springframework.security.oauth2.config.annotation.web.configurers.
AuthorizationServerEndpointsConfigurer;
import
org.springframework.security.oauth2.provider.token.store.JwtAccessToke
nConverter;
@Configuration
@EnableAuthorizationServer
public class OAuthConfiguration extends
AuthorizationServerConfigurerAdapter {
@Value ( "${jwt.clientId:glee-o-meter}" )
private String clientId;
@Value ( "${jwt.client-secret:secret}" )
private String clientSecret;
@Value ( "${jwt.signing-key:123}" )
private String jwtSigningKey;
@Value ( "$
{jwt.authorizedGrantTypes:password,authorization_code,refresh_token}" )
private String[] authorizedGrantTypes;
public OAuthConfiguration(AuthenticationManager
authenticationManager, PasswordEncoder passwordEncoder,
UserDetailsService userService) {
this .authenticationManager = authenticationManager;
this .passwordEncoder = passwordEncoder;
this .userService = userService;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
clients.inMemory()
.withClient(clientId)
.secret(passwordEncoder.encode(clientSecret))
.accessTokenValiditySeconds(accessTokenValiditySeconds)
.refreshTokenValiditySeconds(refreshTokenValiditySeconds)
.authorizedGrantTypes(authorizedGrantTypes)
.scopes( "read" , "write" )
.resourceIds( "api" );
}
@Override
public void configure(final AuthorizationServerEndpointsConfigurer
endpoints) {
endpoints
.accessTokenConverter(accessTokenConverter())
.userDetailsService(userService)
.authenticationManager(authenticationManager);
}
@Bean
JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new
JwtAccessTokenConverter();
return converter;
}
The next section describes the configuration to apply to the resource server.
package net.reliqs.gleeometer.security;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.oauth2.config.annotation.web.configuratio
n.EnableResourceServer;
import
org.springframework.security.oauth2.config.annotation.web.configuratio
n.ResourceServerConfigurerAdapter;
import
org.springframework.security.oauth2.config.annotation.web.configurers.
ResourceServerSecurityConfigurer;
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends
ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources)
{
resources.resourceId( "api" );
}
The last configuration element is about the definition of web application security.
import net.reliqs.gleeometer.errors.CustomAccessDeniedHandler;
import net.reliqs.gleeometer.errors.CustomAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import
org.springframework.security.authentication.AuthenticationManager;
import
org.springframework.security.authentication.dao.DaoAuthenticationProvi
der;
import
org.springframework.security.config.annotation.method.configuration.En
ableGlobalMethodSecurity;
import
org.springframework.security.config.annotation.web.builders.HttpSecuri
ty;
import
org.springframework.security.config.annotation.web.configuration.Enabl
eWebSecurity;
import
org.springframework.security.config.annotation.web.configuration.WebSe
curityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import
org.springframework.security.core.userdetails.UserDetailsService;
import
org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity (prePostEnabled = true , proxyTargetClass =
true )
public class ServerSecurityConfig extends WebSecurityConfigurerAdapter
{
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new
DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(userDetailsService);
return provider;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws
Exception {
return super .authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATE
LESS)
.and()
.authorizeRequests()
.antMatchers( "/api/signin/**" ).permitAll()
.antMatchers( "/api/glee/**" ).hasAnyAuthority( "ADMIN" ,
"USER" )
.antMatchers( "/api/users/**" ).hasAuthority( "ADMIN" )
.antMatchers( "/api/**" ).authenticated()
.anyRequest().authenticated()
.and().exceptionHandling().authenticationEntryPoint(customAuthenticati
onEntryPoint).accessDeniedHandler( new CustomAccessDeniedHandler());
}
authentication.
package net.reliqs.gleeometer.security;
import net.reliqs.gleeometer.users.User;
import net.reliqs.gleeometer.users.UserRepository;
import org.springframework.security.core.GrantedAuthority;
import
org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import
org.springframework.security.core.userdetails.UserDetailsService;
import
org.springframework.security.core.userdetails.UsernameNotFoundExceptio
n;
import org.springframework.stereotype.Service;
@Service
public class UserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws
UsernameNotFoundException {
User user = repository.findByEmail(username).orElseThrow(() ->
new RuntimeException( "User not found: " + username));
GrantedAuthority authority = new
SimpleGrantedAuthority(user.getRole().name());
return new
org.springframework.security.core.userdetails.User(user.getEmail(),
user.getPassword(), Arrays.asList(authority));
}
}
REST Controller
Inside the REST controller we can find two ways to apply access control for each
resource method:
parameter
Using @PreAuthorize or @PostAuthorize annotations
package net.reliqs.gleeometer.users;
import lombok.extern.slf4j.Slf4j;
import net.reliqs.gleeometer.errors.EntityNotFoundException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.crypto.password.PasswordEncoder;
import
org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.ConstraintViolationException;
import javax.validation.Valid;
import javax.validation.constraints.Size;
import java.util.HashSet;
@RestController
@RequestMapping ( "/api/users" )
@Slf 4j
@Validated
class UserController {
@GetMapping
Page<User> all(@PageableDefault(size = Integer.MAX_VALUE) Pageable
pageable, OAuth2Authentication authentication) {
String auth = (String)
authentication.getUserAuthentication().getPrincipal();
String role =
authentication.getAuthorities().iterator().next().getAuthority();
if (role.equals(User.Role.USER.name())) {
return repository.findAllByEmail(auth, pageable);
}
return repository.findAll(pageable);
}
@GetMapping ( "/search" )
Page<User> search(@RequestParam String email, Pageable pageable,
OAuth2Authentication authentication) {
String auth = (String)
authentication.getUserAuthentication().getPrincipal();
String role =
authentication.getAuthorities().iterator().next().getAuthority();
if (role.equals(User.Role.USER.name())) {
return repository.findAllByEmailContainsAndEmail(email,
auth, pageable);
}
return repository.findByEmailContains(email, pageable);
}
@GetMapping ( "/findByEmail" )
@PreAuthorize ( "!hasAuthority('USER') || (authentication.principal
== #email)" )
User findByEmail(@RequestParam String email, OAuth2Authentication
authentication) {
return repository.findByEmail(email).orElseThrow(() -> new
EntityNotFoundException(User .class, "email", email)) ;
}
@GetMapping ( "/{id}" )
@PostAuthorize ( "!hasAuthority('USER') || (returnObject != null &&
returnObject.email == authentication.principal)" )
User one(@PathVariable Long id) {
return repository.findById(id).orElseThrow(() -> new
EntityNotFoundException(User .class, "id", id.toString())) ;
}
@PutMapping ( "/{id}" )
@PreAuthorize ( "!hasAuthority('USER') || (authentication.principal
== @userRepository.findById(#id).orElse(new
net.reliqs.gleeometer.users.User()).email)" )
void update(@PathVariable Long id, @Valid @RequestBody User res) {
User u = repository.findById(id).orElseThrow(() -> new
EntityNotFoundException(User .class, "id", id.toString())) ;
res.setPassword(u.getPassword());
res.setGlee(u.getGlee());
repository.save(res);
}
@PostMapping
@PreAuthorize ( "!hasAuthority('USER')" )
User create(@Valid @RequestBody User res) {
return repository.save(res);
}
@DeleteMapping ( "/{id}" )
@PreAuthorize ( "!hasAuthority('USER')" )
void delete(@PathVariable Long id) {
if (repository.existsById(id)) {
repository.deleteById(id);
} else {
throw new EntityNotFoundException(User .class, "id",
id.toString()) ;
}
}
@PutMapping ( "/{id}/changePassword" )
@PreAuthorize ( "!hasAuthority('USER') || (#oldPassword != null && !
#oldPassword.isEmpty() && authentication.principal ==
@userRepository.findById(#id).orElse(new
net.reliqs.gleeometer.users.User()).email)" )
void changePassword(@PathVariable Long id, @RequestParam(required =
false) String oldPassword, @Valid @Size(min = 3) @RequestParam String
newPassword) {
User user = repository.findById(id).orElseThrow(() -> new
EntityNotFoundException(User .class, "id", id.toString())) ;
if (oldPassword == null || oldPassword.isEmpty() ||
passwordEncoder.matches(oldPassword, user.getPassword())) {
user.setPassword(passwordEncoder.encode(newPassword));
repository.save(user);
} else {
throw new ConstraintViolationException( "old password
doesn't match" , new HashSet<>());
}
}
}
Conclusion
Spring Security and Spring Boot permit to quickly set up a complete OAuth2
authorization/authentication server in an almost declarative manner. The setup can
be further shortened by configuring OAuth2 client’s properties directly
from application.properties/yml file, as explained in this tutorial.
What is OAuth2?
What is JWT?
JWT stands for JSON Web Token, a specification for the representation of claims to
be transferred between two parties. The claims are encoded as a JSON object used as
the payload of an encrypted structure which enables the claims to be digitally signed
or encrypted.
Spring Boot is an opinionated view of the Spring platform and third-party libraries
which permits to minimize the configuration of Spring-based application while
maintaining production-grade quality level.