DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workloads.

Related

  • Authentication With Remote LDAP Server in Spring Web MVC
  • Spring Security Oauth2: Google Login
  • Full-Duplex Scalable Client-Server Communication with WebSockets and Spring Boot (Part I)
  • Develop a Secure CRUD Application Using Angular and Spring Boot

Trending

  • Efficient API Communication With Spring WebClient
  • Intro to RAG: Foundations of Retrieval Augmented Generation, Part 1
  • Docker Base Images Demystified: A Practical Guide
  • The Evolution of Scalable and Resilient Container Infrastructure
  1. DZone
  2. Coding
  3. Frameworks
  4. Build a Secure App Using Spring Boot and WebSockets

Build a Secure App Using Spring Boot and WebSockets

By 
Jimena Garbarino user avatar
Jimena Garbarino
·
Dec. 04, 19 · Tutorial
Likes (6)
Comment
Save
Tweet
Share
56.7K Views

Join the DZone community and get the full member experience.

Join For Free

security-camera

A WebSocket is a bi-directional computing communication channel between a client and server, which is great when the communication is low-latency and high-frequency. Websockets are mainly used in joint, event-driven, or live apps, where the speed of the conventional client-server request-response model doesn’t meet the credentials. Examples include team dashboards and stock trading applications. 

To start, let’s take a look at how the WebSocket protocol works and how to deal with messages with STOMP. Then, you’ll develop an app with Spring Boot and WebSockets and secure them with Okta for authentication and access tokens. You’ll also use the WebSocket API to compose a Java/Spring Boot message broker and verify a JavaScript STOMP client during the WebSocket exchange. Finally, we’re going to add some awesome frameworks to the app so that it plays cool music loops. 

You may also like: Managing Live WebSocket API Clients

The WebSocket Protocol and HTTP

The WebSocket protocol, defined in RFC 6455, consists of an opening handshake, followed by basic message framing, all over TCP. Although it is not HTTP, WebSockets works over HTTP and begins with a client HTTP request with an Upgrade header to switch to the WebSocket protocol:

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13


The response from the server looks like this:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat


After the handshake comes the data transfer phase, during which each side can send data frames, or messages. The protocol defines message types binary and text but does not define their contents. However, it does define the mechanism for sub-protocol negotiation, STOMP.

STOMP — Simple Text Oriented Messaging Protocol

STOMP (Simple Text Oriented Messaging Protocol) was born as an alternative to existing open messaging protocols, like AMQP, to enterprise message brokers from scripting languages like Ruby, Python, and Perl with a subset of common message operations.

STOMP enables a simple publish-subscribe interaction over WebSockets and defines SUBSCRIBE and SEND commands with a destination header. These are inspected by the broker for message dispatching.

The STOMP frame contains a command string, header entries, and a body:

COMMAND
header1:value1
header2:value2

Body^@


Spring Support for WebSockets

Happily, for Java developers, Spring supports the WebSocket API, which implements raw WebSockets, WebSocket emulation through SocksJS (when WebSockets are not supported), and publish-subscribe messaging through STOMP. In this tutorial, you will learn how to use the WebSockets API and configure a Spring Boot message broker. Then we will authenticate a JavaScript STOMP client during the WebSocket handshake and implement Okta as an authentication and access token service. Let’s go!

Spring Boot Example App - Sound Looping!

For our example application, let’s create a collaborative sound-looping UI, where all connected users can play and stop a set of music loops. We will use Tone.js and NexusUI and configure a Spring Message Broker Server and JavaScript WebSocket Client. Rather than building authentication and access control yourself, register for an Okta Developer Account. It’s free!

Once you’ve logged in to Okta, go to the Applications section, and create an application:

  • Choose SPA (Single Page Application) and click Next.
  • On the next page, add http://localhost:8080 as a Login redirect URI.
  • Copy the Client ID from the General Settings.

Create the Message Broker Server Application in Spring Boot

Let’s get started with the application skeleton. Create a Spring Boot application with Spring Initializr and add the Okta Spring Boot Starter and WebSocket dependencies.

curl https://start.spring.io/starter.zip -d dependencies=websocket,okta \
-d language=java \
-d type=maven-project \
-d groupId=com.okta.developer \
-d artifactId=java-websockets  \
-d name="Java WebSockets" \
-d description="Demo project of Spring support for Java WebSockets" \
-d packageName=com.okta.developer.websockets \
-o java-websockets.zip


Click the downloaded .zip file to expand it, or use the following command:

unzip java-websockets.zip -d java-websockets


For the built-in broker with authentication to work, add the following additional dependencies to your pom.xml:

<dependency>
   <groupId>org.springframework.security</groupId>
   <artifactId>spring-security-messaging</artifactId>
</dependency>


Configure the built-in STOMP broker with a WebSocketBrokerConfig.java class and add the following code to it. The @EnableWebSocketMessageBroker annotation enables WebSocket support in a Spring Boot app.

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketBrokerConfig implements WebSocketMessageBrokerConfigurer {

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

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/looping")
                .withSockJS();
    }

}


In the configuration above, the /looping connection endpoint initiates a WebSocket handshake and the /topic endpoint handles publish-subscribe interactions.

Token-Based Authentication for Server Side Java

NOTE: Spring Security requires authentication performed in the web application to hand off the principal to the WebSocket during the connection. For this example, we will use a different approach and configure Okta authentication to obtain an access token the client will send to the server during the WebSockets handshake. This allows you to have unique sessions in the same browser. If we only used server-side authentication, your browser tabs would share the same session.

First, configure WebSocket security and request authentication for any message. To do this, create a WebSocketSecurityConfig class to extend AbstractSecurityWebSocketMessageBrokerConfigurer. Override the configureInbound() method to require authentication for all requests, and disable the same-origin policy by overriding sameOriginDisabled().

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry;
import org.springframework.security.config.annotation.web.socket.AbstractSecurityWebSocketMessageBrokerConfigurer;

@Configuration
public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {

    @Override
    protected void configureInbound(MessageSecurityMetadataSourceRegistry messages) {
        messages.anyMessage().authenticated();
    }

    @Override
    protected boolean sameOriginDisabled() {
        return true;
    }

}


For token-based authentication with STOMP and WebSockets the server must register a custom authentication interceptor. The interceptor must have precedence over Spring Security filters, so it must be declared in its own configurer with the highest order. Create a WebSocketAuthenticationConfig class with the following code:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.simp.config.ChannelRegistration;
import org.springframework.messaging.simp.stomp.StompCommand;
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
import org.springframework.messaging.support.ChannelInterceptor;
import org.springframework.messaging.support.MessageHeaderAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;

import java.util.List;

@Configuration
@EnableWebSocketMessageBroker
@Order(Ordered.HIGHEST_PRECEDENCE + 99)
public class WebSocketAuthenticationConfig implements WebSocketMessageBrokerConfigurer {

    private static final Logger logger = LoggerFactory.getLogger(WebSocketAuthenticationConfig.class);

    @Autowired
    private JwtDecoder jwtDecoder;

    @Override
    public void configureClientInboundChannel(ChannelRegistration registration) {
        registration.interceptors(new ChannelInterceptor() {
            @Override
            public Message<?> preSend(Message<?> message, MessageChannel channel) {
                StompHeaderAccessor accessor =
                        MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
                if (StompCommand.CONNECT.equals(accessor.getCommand())) {
                    List<String> authorization = accessor.getNativeHeader("X-Authorization");
                    logger.debug("X-Authorization: {}", authorization);

                    String accessToken = authorization.get(0).split(" ")[1];
                    Jwt jwt = jwtDecoder.decode(accessToken);
                    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
                    Authentication authentication = converter.convert(jwt);
                    accessor.setUser(authentication);
                }
                return message;
            }
        });
    }
}


The JwtDecoder will parse and decode the token. To verify the signature, it will retrieve and cache the signing key from the issuer.

Create a src/main/resources/application.yml to hold your Okta issuer. This endpoint will be used to validate JWTs.

okta:
  oauth2:
    issuer: https://{yourOktaDomain}/oauth2/default


Note: The value you should use in place of https://{yourOktaDomain} can be found on your Okta dashboard in the top right.

Finally, to serve the JavaScript client from the same application, configure Spring Security to allow unauthenticated access to the static resources:

import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@Order(1)
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/index.html", "/webjars/**", "/js/**").permitAll();
    }
}


JavaScript WebSocket Client

For simplicity, let’s create a static HTML index page to act as the client end for the WebSocket interaction. First, add WebJars dependencies for Bootstrap, SocksJS, and STOMP.

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>webjars-locator-core</artifactId>
    <version>0.38</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>sockjs-client</artifactId>
    <version>1.1.2</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>stomp-websocket</artifactId>
    <version>2.3.3</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>bootstrap</artifactId>
    <version>4.3.1</version>
</dependency>
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>


Then, create an index.html page in src/main/resources/static:

<!DOCTYPE html>
<html>
<head>
    <title>Looping</title>
    <script src="https://ok1static.oktacdn.com/assets/js/sdk/okta-auth-js/2.0.1/okta-auth-js.min.js" type="text/javascript"></script>
    <script src="/js/auth.js"></script>
    <link href="/webjars/bootstrap/css/bootstrap.min.css" rel="stylesheet">
    <script src="/webjars/jquery/jquery.min.js"></script>
    <script src="/webjars/sockjs-client/sockjs.min.js"></script>
    <script src="/webjars/stomp-websocket/stomp.min.js"></script>
    <script src="/js/NexusUI.js"></script>
    <script src="/js/Tone.js"></script>
    <script src="/js/app.js"></script>
    <link href="https://fonts.googleapis.com/css?family=Permanent+Marker&display=swap" rel="stylesheet">
    <style>
        h1 {
            font-family: 'Permanent Marker', cursive;
        }
    </style>
</head>
<body>
<noscript>
<h2 style="color: #ff0000">It seems your browser doesn't support JavaScript! WebSocket relies on JavaScript being enabled. Please enable JavaScript and reload this page!</h2>
</noscript>
<div id="main-content" class="container">
    <div class="row my-2">
        <div class="col-md-12 text-center">
            <button id="connect" class="btn btn-primary" onclick="connect()" type="button">Connect</button>
            <button id="disconnect" class="btn btn-primary" onclick="disconnect()" type="button">Disconnect</button>
        </div>
    </div>
    <div class="row my-5"></div>
    <div class="row my-2 justify-content-md-center">
        <div class="col-md-12 text-center">
            <h1>Loop me in</h1>
        </div>
    </div>
    <div class="row justify-content-md-center my-2">
        <div class="col col-lg-1 col-sm-2">
        </div>
        <div class="col-md-auto text-center">
                <span id="button-1"></span>
                <span id="button-2"></span>
                <span id="button-3"></span>
                <span id="button-4"></span>
                <span id="button-5"></span>
                <span id="button-6"></span>
                <span id="button-7"></span>
                <span id="button-8"></span>
                <span id="button-9"></span>
        </div>
        <div class="col col-lg-1 col-sm-2">
        </div>
    </div>
</div>
<script src="/js/loop-ui.js"></script>
</body>
</html>


JavaScript Client Authentication

First, create a src/main/resources/static/js folder in your project for the JavaScript files.

Add Tone.js to src/main/resources/static/js. Tone.js is a JavaScript framework to create interactive music in the browser; it will be used to play, stop, and restart the music loops. Download Tone.js from Github.

Add NexusUI also to src/main/resources/static/js. NexusUI is a framework for building web audio instruments, such as dials and sliders, in the browser. In this example, we will create simple circular buttons, each one to play a different loop. Download NexusUI from Github.

Add an auth.js script to handle client authentication with the Okta Authentication SDK. Use the Client ID you copied from the SPA application in the Okta developer console, and also your Org URL. If the client has not authenticated, it will be redirected to the Okta login page. After login and redirect to “/”, and the ID token and access token will be parsed from the URL and added to the token manager.

var authClient = new OktaAuth({
  url: 'https://{yourOktaDomain}',
  issuer: 'https://{yourOktaDomain}/oauth2/default',
  clientId: '{yourClientID}',
  redirectUri: 'http://localhost:8080'
});

var accessToken = authClient.tokenManager.get('accessToken')
  .then(accessToken => {
    // If access token exists, output it to the console
    if (accessToken) {
      console.log(`access_token ${accessToken.accessToken}!`);
    // If ID Token isn't found, try to parse it from the current URL
    } else if (location.hash) {
      authClient.token.parseFromUrl().then(function success(res){
        var accessToken = res[0];
        var idToken = res[1];

        authClient.tokenManager.add('accessToken', accessToken);
        authClient.tokenManager.add('idToken', idToken);

        window.location.hash='';
      });
    }
    else {
      // You're not logged in, you need a sessionToken
      authClient.token.getWithRedirect({
        responseType: ['token','id_token']
      });
    }
  });


Create a src/main/resources/static/js/app.js file with the SocksJS client functionality. The connect() function will retrieve the access token from the token manager and set it in a custom header sent for the CONNECT STOMP command. The client inbound channel interceptor on the server will process this header. Once connected, the client subscribes to the /topic/loops channel. For this example, incoming messages contain a button toggle event.

var stompClient = null;

function connect() {
    authClient.tokenManager.get('accessToken').then(function(accessToken){
        if(accessToken){
            var socket = new SockJS('/looping');
            stompClient = Stomp.over(socket);
            stompClient.connect({"X-Authorization": "Bearer " + accessToken.accessToken}, function (frame) {
                console.log('Connected: ' + frame);
                stompClient.subscribe('/topic/loops', function (message) {
                    console.log(loopEvent);
                    var loopEvent = JSON.parse(message.body);
                    console.log(loopEvent);
                    var button = eval(loopEvent.loopId);
                    if (button.state !== loopEvent.value) {
                        button.state = loopEvent.value;
                        if (loopEvent.value === true) {
                            button.player.restart();
                        } else {
                            button.player.stop();
                        }
                    }
                });
            });
        } else {
            console.log("token expired");
        }
    })
}

function sendEvent(loopId, value){
    if (stompClient != null) {
        stompClient.send("/topic/loops", {}, JSON.stringify({'loopId': loopId, 'value': value}));
    }
}

function disconnect() {
    if (stompClient !== null) {
        stompClient.disconnect();
        stompClient = null;
    }
    console.log("Disconnected");
}


So users can interact with the music loops, create a src/main/resources/static/loop-ui.js script for the UI buttons initialization with Tone.js and NexusUI:

var button1 = new Nexus.Button('#button-1',{
  'size': [80,80],
  'mode': 'toggle',
  'state': false
});

button1.colorize("accent", "#FFBE0B");
button1.colorize("fill", "#333");
button1.player = new Tone.Player({"url": "/loops/loop-chill-1.wav", "loop": true, "fadeOut": 1}).toMaster();
button1.on('change', function(v) {
    if (v === true){
        this.player.restart();
    } else {
        this.player.stop();
    }
    sendEvent("button1", v);    
});

var button2 = new Nexus.Button('#button-2',{
  'size': [80,80],
  'mode': 'toggle',
  'state': false
});
button2.colorize("accent", "#FB5607");
button2.colorize("fill", "#333");
button2.player = new Tone.Player({"url": "/loops/loop-drum-1.wav", "loop": true, "fadeOut": 1}).toMaster();
button2.on('change', function(v) {
    if (v === true){
        this.player.restart();
    } else {
        this.player.stop();
    }
    sendEvent("button2", v);
});


In the code above, button1 is set to play /loops/loop-chill-1.wav and button2 will play /loops/loop-drum-1.wav. Optionally, configure the behavior for buttons 3 to 9, each one should play a different loop when toggled on. You can get the loops from the Github repo of this tutorial. To use your own music files, place them in the src/main/resources/static/loops folder. In addition to loop restart and stop, the on change handler will send the toggle event to the loops topic through the server message broker.

Run and Test the Java Application With WebSockets

Run the application with Maven:

./mvnw spring-boot:run


Open two different browser sessions at http://localhost:8080, with developer tools enabled to watch the console for STOMP traces. The app will first redirect to Okta for the login:

User authentication

User authentication

You can log in with the same account in both browser sessions or use different accounts. After you log in, the UI will load the loop buttons. In each browser, click the Connect button on the top to initiate the WebSocket handshake with the server and subscribe to the “loops” topic.

Example user interface

Example user interface

You should see STOMP commands CONNECT and SUBSCRIBE in the web console:

>>> CONNECT
X-Authorization:Bearer eyJraWQiOiJvSXk...
accept-version:1.1,1.0
heart-beat:10000,10000

<<< CONNECTED
version:1.1
heart-beat:0,0
user-name:0oa14trc2aHwBOide357

Connected: CONNECTED
user-name:0oa14trc2aHwBOide357
heart-beat:0,0
version:1.1

>>> SUBSCRIBE
id:sub-0
destination:/topic/loops


NOTE: In some browsers, you might see a 404 response when the browser attempts to declare the source map Tone.js and NexusUI.js, as they are not present in the local server. You can ignore the error for the test.

Once both browsers have connected to the server with WebSockets, toggle a loop circle button in one browser, and the loop will start playing. The button should also toggle in the second browser when receiving the STOMP MESSAGE command:

<<< MESSAGE
destination:/topic/loops
subscription:sub-0
message-id:iqtb3gvf-0
content-length:33

{"loopId":"button1","value":true}


Congrats! You’ve successfully connected a Spring Boot Application with WebSockets.

Learn More About WebSockets and Spring Boot

I hope you enjoyed this WebSocket experiment as much as I did. You can find all this tutorial’s code on Github.

To continue learning about Okta’s WebSockets related technologies and Spring Framework’s support, check out our related blog posts:

  • Full Stack Reactive with Spring WebFlux, WebSockets, and React
  • A Quick Guide to Spring Boot Login Options
  • 10 Excellent Ways to Secure Your Spring Boot Application


Further Reading

  • Steps to Building Authentication and Authorization for RESTful APIs.
  • API Security: Ways to Authenticate and Authorize.
  • Four Most Used REST API Authentication Methods.
Spring Framework WebSocket Spring Boot Spring Security app application authentication

Published at DZone with permission of Jimena Garbarino, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Authentication With Remote LDAP Server in Spring Web MVC
  • Spring Security Oauth2: Google Login
  • Full-Duplex Scalable Client-Server Communication with WebSockets and Spring Boot (Part I)
  • Develop a Secure CRUD Application Using Angular and Spring Boot

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!