Scaling Secure Applications with Spring Session and Redis
Spring Boot and Spring Security have delighted developers with their APIs for quite some time now. Spring Security has done an excellent job of implementing OAuth and OpenID Connect (OIDC) standards for the last few years.
If you’re using Spring Security’s default authorization code flow with OIDC, it’ll establish a session on the server and serve up old fashion session cookies. If you want to scale your services, you’ll need to share session information. This tutorial shows you how to configure a Spring Boot application to store sessions in Redis with Spring Session, so the session can be shared among multiple gateway nodes and is preserved when a node failure happens.
Prerequisites:
Table of Contents
- Build a Microservices Architecture with Spring Session and Redis
- Add Authentication with OpenID Connect
- Configure Spring Session and Redis
- Spring Session Redis with HAProxy Load Balancing
- Learn More About Spring Session, Redis, and JHipster
Build a Microservices Architecture with Spring Session and Redis
Let’s start by building a microservices architecture. With JHipster and JHipster Domain Languange (JDL) you can generate a microservices architecture from a file that describes the applications and entities.
Install JHipster:
npm install -g generator-jhipster@6.10.5
For this tutorial, you can use the JDL sample microservice-ecommerce-store-4-apps
from the JDL samples repository.
Create a folder for the project:
mkdir spring-session-redis
cd spring-session-redis
TIP: If you’re using Oh My Zsh, you can run take spring-session-redis
as an alternative.
Copy microservice-ecommerce-store-4-apps.jdl
to the project folder and rename it to jhipster-redis.jdl
.
wget https://raw.githubusercontent.com/jhipster/jdl-samples/main/microservice-ecommerce-store-4-apps.jdl -O jhipster-redis.jdl
Update the store
, product
, invoice
, and notification
configs to use OAuth 2.0 / OIDC for authentication and Maven as the build tool (shortcut: replace jwt
with oauth2
and gradle
with maven
):
application {
config {
...
authenticationType oauth2,
buildTool maven,
...
}
}
Run the jdl
command:
jhipster jdl jhipster-redis.jdl
After the generation completes, you will see folders invoice
, notification
,product
, and store
—one for each generated application.
Next, create the Docker Compose configuration for all the applications using JHipster’s docker-compose
sub-generator.
Create a folder docker-compose
in the project root and run the sub-generator:
mkdir docker-compose
cd docker-compose
jhipster docker-compose
Choose the following options:
- Type of application: Microservice application
- Type of gateway: JHipster gateway
- Root directory for microservices:
../
(the default) - Choose all applications with your spacebar and arrow keys (invoice, notification, product, store)
- Don’t select any application for clustered databases
- Setup monitoring: No
- Enter an admin password for JHipster Registry
You will see the following WARNING:
WARNING! Docker Compose configuration generated, but no Jib cache found
This means the application images have yet to be built. Go through each application folder and build the images with Maven:
./mvnw -ntp -Pprod verify jib:dockerBuild
The architecture you generated uses OAuth 2.0 for authorization and OpenID Connect (OIDC) for authentication. By default, it’s configured to work with Keycloak in a Docker container. It’s also quite easy to make it work with an Okta developer account.
I’ll show you how to configure Okta as the authentication provider for the store application, which will act as a gateway to the other services and provides a simple UI to manage the entities.
Add Authentication with OpenID Connect
You can register for a free developer account with the following simple commands using the Okta CLI, in the project root folder:
okta register
Provide the required information. Once you register, create a client application in Okta with the following command:
okta apps create
You will be prompted to select the following options:
- Type of Application: 1: Web
- Type of Application (again): 3: JHipster
- Redirect UI: accept the default values provided for you
The OIDC application’s configuration will be generated in the file .okta.env
and it will look like this (except the placeholder values will be populated):
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET="{clientSecret}"
export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI="https://{yourOktaDomain}/oauth2/default"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID="{clientId}"
Edit the file docker-compose/docker-compose.yml
and override the default OAuth 2.0 settings for the services invoice
, notification
, product
, and store
with the following values (you will need to update these properties under the environment
key):
- SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=${OKTA_OAUTH2_ISSUER}
- SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=${OKTA_OAUTH2_CLIENT_ID}
- SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=${OKTA_OAUTH2_CLIENT_SECRET}
By default, the JHipster Registry is configured to use Keycloak in docker-compose/jhipster-registry.yml
. You don’t need to change anything for this to work, but you will be using Keycloak for the JHipster Registry and Okta for the rest of your apps. To fix this, you can update this file with the same settings as above, or remove the oauth2
profile which will cause it to use JWT for authentication (and the default admin/admin
credentials).
Create a file docker-compose/.env
and set the value of the OKTA_OAUTH_*
environment variables for Docker Compose, copying the values from .okta.env
:
OKTA_OAUTH2_ISSUER=https://{yourOktaDomain}/oauth2/default
OKTA_OAUTH2_CLIENT_ID={clientId}
OKTA_OAUTH2_CLIENT_SECRET={clientSecret}
NOTE: You can also set the OAuth 2.0 configuration for all the applications in a single place, using the JHipster Registry, since it’s also a Spring Cloud Config Server. See Java Microservices with Spring Cloud Config and JHipster for more information.
Run the services with Docker Compose:
cd docker-compose
docker-compose up
The JHipster Registry will log the following message once it is ready:
jhipster-registry_1 | 2020-12-11 16:03:47.233 INFO 6 --- [ main] i.g.j.registry.JHipsterRegistryApp :
jhipster-registry_1 | ----------------------------------------------------------
jhipster-registry_1 | Application 'jhipster-registry' is running! Access URLs:
jhipster-registry_1 | Local: http://localhost:8761/
jhipster-registry_1 | External: http://172.18.0.2:8761/
jhipster-registry_1 | Profile(s): [composite, dev, swagger, oauth2]
jhipster-registry_1 | ----------------------------------------------------------
jhipster-registry_1 | 2020-12-11 16:03:47.234 INFO 6 --- [ main] i.g.j.registry.JHipsterRegistryApp :
jhipster-registry_1 | ----------------------------------------------------------
jhipster-registry_1 | Config Server: Connected to the JHipster Registry running in Docker
jhipster-registry_1 | ----------------------------------------------------------
You can sign into the JHipster Registry at http://localhost:8761
to check if all services are up:
Once all services are up, access the store at http://localhost:8080
and sign in with your Okta user:
Configure Spring Session and Redis
The store
application maintains a user session in memory, identified with a session ID that is sent in a cookie to the client. If the store instance crashes, the session is lost. One way to avoid losing the session is by adding Spring Session with Redis for the session storage and sharing among store
nodes.
Redis is an open-source, in-memory data structure store—used as a database, cache and message broker. It supports many data structures, has built-in replication, provides high availability, and supports partitioning. A session store requires high availability and durability to support uninterrupted user engagement. Redis is the most loved database of 2020 according to Stack Overflow, and it is a popular choice for session management due to its low latency, scalability, and resilience.
Before making the modifications to the store
application, stop all services with CTRL+C and remove the containers:
cd docker-compose
docker-compose down
Delete the store image:
docker rmi store
Edit the store/pom.xml
and add the Spring Session + Redis dependency:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
<version>6.0.0.RELEASE</version>
</dependency>
Spring Session Data Redis depends on Spring Data Redis, which integrates with Lettuce and Jedis, two popular open-source Java clients for Redis. Spring Data Redis does not pull any client by default, so you need to add the Lettuce dependency explicitly.
To enable Redis for your Spring profiles, add the following configuration to store/src/main/resources/config/application-dev.yml
and store/src/main/resources/config/application-prod.yml
:
spring:
session:
store-type: redis
For this example, disable Redis in the store’s test configuration, so the existing tests don’t require a Redis instance. Edit src/test/resources/config/application.yml
and add the following:
spring:
session:
store-type: none
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration
Rebuild the store
application image:
cd ../store
./mvnw -ntp -Pprod verify jib:dockerBuild
Edit docker-compose/docker-compose.yml
to set the Redis configuration. Under the store
service entry, add the following variables to the environment:
- LOGGING_LEVEL_COM_JHIPSTER_DEMO_STORE=TRACE
- SPRING_REDIS_HOST=store-redis
- SPRING_REDIS_PASSWORD=password
- SPRING_REDIS_PORT=6379
Add the store-redis
instance as a new service (at the bottom of the file, and indent two spaces):
store-redis:
image: 'redis:6.0.8'
command: redis-server --requirepass password
ports:
- '6379:6379'
Run the service again with Docker Compose:
cd ../docker-compose
docker-compose up
Once all services are up, sign in to the store
application with your Okta account. Then, confirm Redis has stored new session keys:
docker exec docker-compose_store-redis_1 redis-cli -a password KEYS \*
The output should look like this:
spring:session:sessions:21eb225f-f92e-4a9b-937b-28162fd14c20
spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:00u1h70sf0trRsbLS357
spring:session:sessions:expires:21eb225f-f92e-4a9b-937b-28162fd14c20
spring:session:expirations:1604972940000
Spring Session Redis with HAProxy Load Balancing
Load balancing in a JHipster microservices architecture is handled at the client-side, where the client is the Spring Boot instance. The JHipster Registry is a Eureka discovery server, maintaining a dynamic list of available service instances, for the service clients to do request routing and load balancing. The store service, which acts as a gateway and also as a Eureka client, requests the available instances to the JHipster registry for service routing.
As you want to test session sharing among multiple store
nodes, you need load balancing for the store
service as well. You’ll need to run an HAProxy container and two instances of the store
service for this test.
Stop all services and remove the store container with docker rm docker-compose_store_1
before starting the modifications below.
First, extract the docker-compose store
base configuration to its own docker-compose/store.yml
file:
version: '2'
services:
store:
image: store
environment:
- _JAVA_OPTIONS=-Xmx512m -Xms256m
- 'SPRING_PROFILES_ACTIVE=prod,swagger'
- MANAGEMENT_METRICS_EXPORT_PROMETHEUS_ENABLED=true
- 'EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://admin:$${jhipster.registry.password}@jhipster-registry:8761/eureka'
- 'SPRING_CLOUD_CONFIG_URI=http://admin:$${jhipster.registry.password}@jhipster-registry:8761/config'
- 'SPRING_DATASOURCE_URL=jdbc:mysql://store-mysql:3306/store?useUnicode=true&characterEncoding=utf8&useSSL=false&useLegacyDatetimeCode=false&serverTimezone=UTC&createDatabaseIfNotExist=true'
- SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=${OKTA_OAUTH2_ISSUER}
- SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=${OKTA_OAUTH2_CLIENT_ID}
- SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=${OKTA_OAUTH2_CLIENT_SECRET}
- JHIPSTER_SLEEP=30
- JHIPSTER_REGISTRY_PASSWORD=admin
- LOGGING_LEVEL_COM_JHIPSTER_DEMO_STORE=TRACE
- SPRING_REDIS_HOST=store-redis
- SPRING_REDIS_PASSWORD=password
- SPRING_REDIS_PORT=6379
Then, edit docker-compose/docker-compose.yml
and remove the store
service. Instead create store1
and store2
services, extending the base configuration. Add the HAProxy service as well.
services:
...
store1:
extends:
file: store.yml
service: store
hostname: store1
ports:
- '8080:8080'
store2:
extends:
file: store.yml
service: store
hostname: store2
ports:
- '8081:8080'
haproxy:
extends:
file: haproxy.yml
service: haproxy
Create the HAProxy base configuration at docker-compose/haproxy.yml
with the following content:
version: '2'
services:
haproxy:
build:
context: .
dockerfile: Dockerfile-haproxy
image: haproxy
ports:
- 80:80
Create a docker-compose/Dockerfile-haproxy
file to specify how Docker should build the HAProxy image:
FROM haproxy:2.3
COPY haproxy.cfg /usr/local/etc/haproxy/haproxy.cfg
Create the file docker-compose/haproxy.cfg
with the HAProxy service configuration:
global
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 store1 store1:8080 cookie store1
server store2 store2:8080 cookie store2
In the configuration above, store1
and store2
are the backend servers to load balance with a round-robin strategy. With option redispatch, HAProxy will re-dispatch the request to another server if the selected server fails.
HAProxy listens on port 80 so you’ll need to update your Okta application. Run okta login
, open the resulting URL in your browser, and go to Applications. Select your application and add
http://localhost/login/oauth2/code/oidc
as a Login redirect URI, and http://localhost
as a Logout redirect URI.
Run all your Spring services again:
docker-compose up
Once all services are up, sign in to http://localhost
with your credentials and navigate to Entities > Product. In your browser’s developer console, check the SERVERUSED cookie by typing `document.cookie’. You should output like the following:
"SERVERUSED=store2; XSRF-TOKEN=6aa9ebef-b62f-40b5-9310-3e9d74042fa6"
Stop the container of that store
instance:
docker stop docker-compose_store2_1
TIP: If you get a “No such container” error, run docker ps --format '{{.Names}}'
to print your container names. For example, it might be named docker-compose_store2_1
.
Create a new entity and inspect the POST request to verify that a different server responds, without losing the session:
SERVERUSED=store1
Did it work? If so, give yourself a big pat on the back!
Learn More About Spring Session, Redis, and JHipster
I hope you enjoyed this tutorial and helped you understand one possible approach to session sharing in JHipster with Spring Session. Keep learning and check the following links for more:
You can find all the code for this tutorial in our okta-spring-session-redis-example repository.
If you liked this tutorial, you might like these:
- Easy Session Sharing in Spring Boot with Spring Session and MySQL
- Build a Secure Micronaut and Angular App with JHipster
- OAuth 2.0 Patterns with Spring Cloud Gateway
- Secure Secrets With Spring Cloud Config and Vault
- Communicate Between Microservices with Apache Kafka
If you have any questions about this post, please leave a comment below. For more hipster content, follow @oktadev on Twitter, like us on LinkedIn, or subscribe to our YouTube channel.