Easy Session Sharing in Spring Boot With Spring Session and MySQL
In this tutorial, we will walk you through the configuration of session sharing in a multi-node Spring Boot application.
Join the DZone community and get the full member experience.
Join For FreeSession management in multi-node applications presents multiple challenges. When the architecture includes a load balancer, client requests might be routed to different servers each time, and the HTTP session might be lost. In this tutorial, we will walk you through the configuration of session sharing in a multi-node Spring Boot application.
Prerequisites: Table of ContentsSession Persistence
Session Persistence is a technique for sticking a client to a single server, using application layer information-like a cookie, for example. In this tutorial, we will implement session persistence with the help of HAProxy, a reliable, high performance, TCP/HTTP load balancer.

First, let's create a web application with Okta authentication and run three nodes with HAProxy load balancing using Docker Compose.
Create a Maven project using the Spring Initializr's API.
curl https://start.spring.io/starter.zip \
-d dependencies=web,okta \
-d groupId=com.okta.developer \
-d artifactId=webapp \
-d name="Web Application" \
-d description="Demo Web Application" \
-d packageName=com.okta.developer.webapp \
-d javaVersion=11 \
-o web-app.zip
Unzip the project:
xxxxxxxxxx
unzip web-app.zip -d web-app
cd web-app
Run the Okta Maven Plugin to register a new account:
xxxxxxxxxx
./mvnw com.okta:okta-maven-plugin:register
If you already have an Okta account registered, use login
instead of register
.
Then, configure your Spring application for authentication using Okta:
xxxxxxxxxx
./mvnw com.okta:okta-maven-plugin:spring-boot
It will set up a new OIDC application for you and write your Okta settings to your src/main/resources/application.properties
file.
Create a GreetingController
at src/main/java/com/okta/developer/webapp/controller
:
xxxxxxxxxx
package com.okta.developer.webapp.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.context.request.RequestContextHolder;
import java.net.InetAddress;
public class GreetingController {
private static final Logger logger = LoggerFactory.getLogger(GreetingController.class);
value = "/greeting") (
public String getGreeting( OidcUser oidcUser) {
String serverUsed = "unknown";
try {
InetAddress host = InetAddress.getLocalHost();
serverUsed = host.getHostName();
} catch (Exception e){
logger.error("Could not get hostname", e);
}
String sessionId = RequestContextHolder.currentRequestAttributes().getSessionId();
logger.info("Request responded by " + serverUsed);
return "Hello " + oidcUser.getFullName() + ", your server is " + serverUsed + ", with sessionId " + sessionId;
}
}
Run the application with:
xxxxxxxxxx
./mvnw spring-boot:run
Go to http://localhost:8080
in an incognito window and you should be redirected to the Okta sign-in page.

If you sign in, you will get a 404 error when you're redirected back to your Spring Boot app. This is expected because there's no controller mapped to the /
endpoint. You can fix this if you want by adding a method like the following to your WebApplication.java
.
x
public class WebApplication {
public static void main(String[] args) {
SpringApplication.run(WebApplication.class, args);
}
"/") (
public String hello( OidcUser user) {
return "Hello, " + user.getFullName();
}
}
Now, let's configure three Docker containers, one for each application node, and an HAProxy container. In the project root folder, create a docker/docker-compose.yml
file, with the following content:
x
version'3.1'
services
webapp1
environment
OKTA_OAUTH2_ISSUER=$ OKTA_OAUTH2_ISSUER
OKTA_OAUTH2_CLIENT_ID=$ OKTA_OAUTH2_CLIENT_ID
OKTA_OAUTH2_CLIENT_SECRET=$ OKTA_OAUTH2_CLIENT_SECRET
image webapp
hostname webapp1
ports
8081:8080
webapp2
environment
OKTA_OAUTH2_ISSUER=$ OKTA_OAUTH2_ISSUER
OKTA_OAUTH2_CLIENT_ID=$ OKTA_OAUTH2_CLIENT_ID
OKTA_OAUTH2_CLIENT_SECRET=$ OKTA_OAUTH2_CLIENT_SECRET
image webapp
hostname webapp2
ports
8082:8080
webapp3
environment
OKTA_OAUTH2_ISSUER=$ OKTA_OAUTH2_ISSUER
OKTA_OAUTH2_CLIENT_ID=$ OKTA_OAUTH2_CLIENT_ID
OKTA_OAUTH2_CLIENT_SECRET=$ OKTA_OAUTH2_CLIENT_SECRET
image webapp
hostname webapp3
ports
8083:8080
haproxy
build
context.
dockerfile Dockerfile-haproxy
image my-haproxy
ports
80:80
depends_on
"webapp1"
"webapp2"
"webapp3"
Create a docker/.env
file with the following content:
xxxxxxxxxx
OKTA_OAUTH2_ISSUER={issuer}
OKTA_OAUTH2_CLIENT_ID={clientId}
OKTA_OAUTH2_CLIENT_SECRET={clientSecret}
You can find the issuer, clientId, and clientSecret in the src/main/resources/application.properties
, after running the Okta Maven Plugin. Remove the \
in the issuer's URL after you paste the value. Also, make sure to remove the curly braces around the values.
Create a Dockerfile
for the HAProxy container, at docker/Dockerfile-haproxy
and add the following:
x
FROM haproxy:2.2
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
Create the configuration file for the HAProxy instance at docker/haproxy.cfg
:
x
global
debug
daemon
maxconn 2000
defaults
mode http
timeout connect 5000ms
timeout client 50000ms
timeout server 50000ms
frontend http-in
bind *:80
default_backend servers
backend servers
balance roundrobin
cookie SERVERUSED insert indirect nocache
option httpchk /
option redispatch
default-server check
server webapp1 webapp1:8080 cookie webapp1
server webapp2 webapp2:8080 cookie webapp2
server webapp3 webapp3:8080 cookie webapp3
I'm not going to dive deep into how to configure HAProxy but take note that, in the backend servers
section, there are the following options:
balance roundrobin
sets round-robin as the load balancing strategy.cookie SERVERUSED
adds a cookie SERVERUSED to the response, indicating the server responding to the request. The client requests will stick to that server.option redispatch
makes the request be re-dispatched to a different server if the current server fails.
Edit the pom.xml
to add the Jib Maven Plugin to the <build>
section to create a webapp
Docker image.
x
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>2.5.2</version>
<configuration>
<to>
<image>webapp</image>
</to>
</configuration>
</plugin>
Build the webapp
container image:
x
./mvnw compile jib:dockerBuild
Start all the services with docker-compose:
x
cd docker
docker-compose up
NOTE: If you get a URISyntaxException
on startup, remove the \
in the issuer in application.properties
.
HAProxy will be ready after you see the following lines in the logs:
xxxxxxxxxx
haproxy_1 | [WARNING] 253/130140 (6) : Server servers/webapp2 is UP, reason: Layer7 check passed, code: 302, check duration: 5ms. 1 active and 0 backup servers online. 0 sessions requeued, 0 total in queue.
haproxy_1 | [WARNING] 253/130141 (6) : Server servers/webapp3 is UP, reason: Layer7 check passed, code: 302, check duration: 4ms. 2 active and 0 backup servers online. 0 sessions requeued, 0 total in queue.
haproxy_1 | [WARNING] 253/130143 (6) : Server servers/webapp1 is UP, reason: Layer7 check passed, code: 302, check duration: 7ms. 3 active and 0 backup servers online. 0 sessions requeued, 0 total in queue.
Before you can sign in to your application, you'll need to go to your Okta developer console and add a Login redirect URI for http://localhost/login/oauth2/code/okta
. Otherwise, you'll get a 400 error in the next step. While you're in there, add a Logout redirect URI for http://localhost
.
In a browser, go to http://localhost/greeting
. After you sign in, inspect the request cookie SERVERUSED
. An example value is:
xxxxxxxxxx
Cookie: SERVERUSED=webapp3; JSESSIONID=5AF5669EA145CC86BBB08CE09FF6E505
Shut down the current node with the following Docker command:
xxxxxxxxxx
docker stop docker_webapp3_1
Refresh your browser and wait a few seconds. Check the SERVERUSED
cookie to verify that HAProxy re-dispatched the request to a different node, and the sessionId has changed, meaning the old session was lost.
You can stop the services with CTRL+C
.
Session Sharing With Spring Session
Storing sessions in an individual node can affect scalability. When scaling up, active sessions will remain in the original nodes and traffic will not be spread equally among nodes. Also, when a node fails, the session in that node is lost. With session sharing, the user session lives in a shared data storage that all server nodes can access.
Next, for a transparent failover with the redispatch
option in HAProxy, let's add session sharing between nodes with Spring Session. For this tutorial, I'll show you how to use MySQL for storing the session.
First, add the following dependencies to the pom.xml
:
xxxxxxxxxx
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
Rename src/main/resources/application.properties
to application.yml
, change your okta.*
properties to be in YAML syntax, and add the following key-value pairs:
xxxxxxxxxx
okta
oauth2
issuer issuer
client-secret client-secret
client-id client-id
spring
session
jdbc
initialize-schema always
datasource
url jdbc mysql //localhost 3306/webapp
username root
password example
driverClassName com.mysql.cj.jdbc.Driver
hikari
initializationFailTimeout0
logging
level
org.springframework INFO
com.zaxxer.hikari DEBUG
In this example, you are using HikariCP for the database connection pooling. The option initializationFailTimeout
is set to 0, meaning if a connection cannot be obtained, the pool will start anyways.
You are also instructing Spring Session to always create the schema with the option spring.session.jdbc.initialize-schema=always
.
The application.yml
file you just created contains the default datasource properties for the MySQL session storage. As the MySQL database is not up when the tests run, set up an in-memory H2 database so the application tests don't fail.
Create a src/test/resources/application-test.yml
file with the following content:
xxxxxxxxxx
spring
datasource
url jdbc h2 mem testdb
username sa
password passord
driverClassName org.h2.Driver
Modify the WebApplicationTests.java
class to add the @ActiveProfiles
annotation:
xxxxxxxxxx
package com.okta.developer.webapp;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
"test") (
class WebApplicationTests {
void contextLoads() {
}
}
Modify docker/docker-compose.yml
to add the database container and the admin application to inspect the session tables. The final configuration should look like the following:
xxxxxxxxxx
version'3.1'
services
webapp1
environment
SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/webapp
OKTA_OAUTH2_ISSUER=$ OKTA_OAUTH2_ISSUER
OKTA_OAUTH2_CLIENT_ID=$ OKTA_OAUTH2_CLIENT_ID
OKTA_OAUTH2_CLIENT_SECRET=$ OKTA_OAUTH2_CLIENT_SECRET
image webapp
hostname webapp1
ports
8081:8080
depends_on
"db"
webapp2
environment
SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/webapp
OKTA_OAUTH2_ISSUER=$ OKTA_OAUTH2_ISSUER
OKTA_OAUTH2_CLIENT_ID=$ OKTA_OAUTH2_CLIENT_ID
OKTA_OAUTH2_CLIENT_SECRET=$ OKTA_OAUTH2_CLIENT_SECRET
image webapp
hostname webapp2
ports
8082:8080
depends_on
"db"
webapp3
environment
SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/webapp
OKTA_OAUTH2_ISSUER=$ OKTA_OAUTH2_ISSUER
OKTA_OAUTH2_CLIENT_ID=$ OKTA_OAUTH2_CLIENT_ID
OKTA_OAUTH2_CLIENT_SECRET=$ OKTA_OAUTH2_CLIENT_SECRET
image webapp
hostname webapp3
ports
8083:8080
depends_on
"db"
db
image mysql
command --default-authentication-plugin=mysql_native_password
restart always
environment
MYSQL_ROOT_PASSWORD example
MYSQL_DATABASE webapp
ports
3306:3306
adminer
image adminer
restart always
ports
8090:8080
haproxy
build
context.
dockerfile Dockerfile-haproxy
image my-haproxy
ports
80:80
depends_on
"webapp1"
"webapp2"
"webapp3"
Delete the previous containers and previous webapp
Docker image with the following commands:
xxxxxxxxxx
docker-compose down
docker rmi webapp
In the root folder of the project, rebuild the webapp docker image with Maven:
xxxxxxxxxx
./mvnw compile jib:dockerBuild
Start all the services again ( docker-compose up
from the docker
directory), and repeat the re-dispatch test (go to http://localhost/greeting
then shutdown the active node with docker stop docker_webapp#_1
). You might see a lot of connection errors until the database is up.
Now the session should be the same after changing the node. How cool is that?!
You can inspect the session data in the admin UI at http://localhost:8090
. Log in with root
and the MYSQL_ROOT_PASSWORD
value that you set in docker-compose.yml
.

Learn More about Spring Session and OAuth 2.0
I hope you enjoyed this tutorial and could see the advantages of the session sharing technique for multi-node applications. You can find all the code for this tutorial in GitHub.
Know that there are multiple options for session storage-we selected a database because of the ease of setup-but it might slow down your application. To learn more about session management, check out the following links:
If you liked this post, follow @oktadev on Twitter to see when we publish similar ones. We have a YouTube channel too! You should subscribe.
Published at DZone with permission of Jimena Garbarino, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments