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

  • Is Spring AI Strong Enough for AI?
  • Comparing the Efficiency of a Spring Boot Project to a Go Project
  • Shift-Left Monitoring Approach for Cloud Apps in Containers
  • Basic Authentication Using Spring Boot Security: A Step-By-Step Guide

Trending

  • Next Evolution in Integration: Architecting With Intent Using Model Context Protocol
  • How to Perform Custom Error Handling With ANTLR
  • Integrating Model Context Protocol (MCP) With Microsoft Copilot Studio AI Agents
  • Building Reliable LLM-Powered Microservices With Kubernetes on AWS
  1. DZone
  2. Coding
  3. Java
  4. Sticky Sessions With Apache APISIX — The Demo

Sticky Sessions With Apache APISIX — The Demo

When it comes to sticky sessions, if necessary, you should replicate the data to other upstreams because this one might go down. This post illustrates it with a demo.

By 
Nicolas Fränkel user avatar
Nicolas Fränkel
DZone Core CORE ·
Jul. 07, 23 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
5.3K Views

Join the DZone community and get the full member experience.

Join For Free

Last week, we described the concept behind sticky sessions: you forward a request to the same upstream because there's context data associated with the session on that node. However, if necessary, you should replicate the data to other upstreams because this one might go down. In this post, we are going to illustrate it with a demo.

The Overall Design

Design options are limitless. I'll keep myself to a familiar stack, the JVM. Also, as mentioned in the previous post, one should only implement sticky sessions with session replication. The final design consists of two components: an Apache APISIX instance with sticky sessions configured and two JVM nodes running the same application with session replication.

The application uses the following:

Dependency Description
Spring Boot Eases the usage of Spring libraries
Spring MVC Allows offering HTTP endpoints
Thymeleaf View technology
Spring Session Offers an abstraction over session replication
Hazelcast (embedded) Implements session replication
Spring Security Binds an identity to a user session

The design looks like the following:

The Overall Design

app1 and app2 are two instances of the same app; I didn't want to overcrowd the diagram with redundant data.

The Heart of the Application

The heart of the application is a session-scoped bean that wraps a counter, which can only be incremented:

Java
 
@Component
@SessionScope
public class Counter implements Serializable {              //1

    private int value = 0;

    public int getValue() {
        return value;
    }

    public void incrementValue() {
        value++;
    }
}


  1. Necessary for Hazelcast serialization to work

We can use this bean in the controller:

Java
 
@Controller
public class IndexController {

    private final Counter counter;

    public IndexController(Counter counter) {               //1
        this.counter = counter;
    }

    @GetMapping("/")
    public String index(Model model) {                      //2
        counter.incrementValue();
        model.addAttribute("counter", counter.getValue());
        return "index";
    }
}

2. Inject the session-scoped bean in the singleton controller thanks to Spring's magic

3. When we send a GET request to the root, increment the counter value and pass it to the model

Finally, we display the bean's value on the Thymeleaf page:

HTML
 
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" lang="en">
<body>
<div th:text="${counter}">3</div>

Configuring Spring Session with Hazelcast

Spring Session offers a filter that wraps the original HttpServletRequest to override the getSession() method. This method returns a specific Session implementation backed by the implementation configured with Spring Session, in our case, Hazelcast.

We need only a few tweaks to configure Spring Session with Hazelcast.

First, annotate the Spring Boot application class with the relevant annotation:

Java
 
@SpringBootApplication
@EnableHazelcastHttpSession
public class SessionApplication {
    ...
}


Hazelcast requires a specific configuration as well. We can use XML, YAML, or code.
Since it's a demo, I can choose whatever I want, so let's code it. Spring Boot requires either an Hazelcast object or a configuration object. The latter is enough:

Java
 
@Bean
public Config hazelcastConfig() {
  var config = new Config();
  var networkConfig = config.getNetworkConfig();
  networkConfig.setPort(0);                                                       //1
  networkConfig.getJoin().getAutoDetectionConfig().setEnabled(true);              //2
  var attributeConfig = new AttributeConfig()                                     //3
          .setName(HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE)
          .setExtractorClassName(PrincipalNameExtractor.class.getName());
  config.getMapConfig(HazelcastIndexedSessionRepository.DEFAULT_SESSION_MAP_NAME) //3
          .addAttributeConfig(attributeConfig)
          .addIndexConfig(new IndexConfig(
            IndexType.HASH,
            HazelcastIndexedSessionRepository.PRINCIPAL_NAME_ATTRIBUTE
  ));
  var serializerConfig = new SerializerConfig();
  serializerConfig.setImplementation(new HazelcastSessionSerializer())            //3
                  .setTypeClass(MapSession.class);
  config.getSerializationConfig().addSerializerConfig(serializerConfig);
  return config;
}


  1. Choose a random port to avoid port conflict
  2. Allow Hazelcast to search for other instances and automatically form a cluster. It's going to be necessary when deployed as per our design
  3. Copy-pasted from the Spring Session documentation

Configuring Spring Security

Most Spring Session examples somehow use Spring Security, and though it's not strictly necessary, it makes the design easier. I want to explain why first.

One can think about sessions as a gigantic hash table. In regular applications, the key is the JSESSIONID cookie value, the value, another hash table of session data. However, the JSESSIONID is specific to the node. The app will give a different JSESSIONID if one uses another node. Since the key is different, there's no way to access the session data, even if the session data is shared across nodes. To prevent this loss, we need to come up with a different key. Spring Security allows using a principal (or the login name) as the session data key.

Here's how I set up a basic Spring Security configuration:

Java
 
@Bean
public SecurityFilterChain securityFilterChain(UserDetailsService service, HttpSecurity http) throws Exception {
  return http.userDetailsService(service)                                    //1
             .authorizeHttpRequests(authorize -> authorize.requestMatchers(
                 PathRequest.toStaticResources().atCommonLocations())        //2
                            .permitAll()                                     //2
                            .anyRequest().authenticated()                    //3
             ).formLogin(form -> form.permitAll()
                                     .defaultSuccessUrl("/")                 //4
             ).build();
}


  1. The default in-memory user details service doesn't allow custom user details classes. I had to provide my own.
  2. Allow everybody to access static resources at "common" locations
  3. All other requests must be authenticated
  4. Allow everybody to access the authentication form
  5. Redirect to the root if successful, which maps the above controller

Putting Our Design to the Test

Beside the counter, I want to display two additional pieces of data: the hostname and the logged-in user.

For the hostname, I add the following method to the controller:

Java
 
@ModelAttribute("hostname")
private String hostname() throws UnknownHostException {
    return InetAddress.getLocalHost().getHostName();
}


Displaying the logged-in user requires an additional dependency:

XML
 
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>


On the page, it's straightforward:

HTML
 
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">  <!--1-->
<body>
<td sec:authentication="principal.label">Me</td>                    <!--2-->


  1. Add the sec namespace. It's not necessary but may help the IDE to help you
  2. Require the underlying UserDetail implementation to have a getLabel() method

Last but not least, we need to configure Apache APISIX with sticky sessions, as we saw last week:

YAML
 
routes:
  - uri: /*
    upstream:
      nodes:
        "webapp1:8080": 1
        "webapp2:8080": 1
      type: chash
      hash_on: cookie
      key: cookie_JSESSIONID
#END


Here's the design implemented on Docker Compose:

YAML
 
services:
  apisix:
    image: apache/apisix:3.3.0-debian
    volumes:
      - ./apisix/config.yml:/usr/local/apisix/conf/config.yaml:ro
      - ./apisix/apisix.yml:/usr/local/apisix/conf/apisix.yaml:ro    #1
    ports:
      - "9080:9080"                                                  #2
    depends_on:
      - webapp1
      - webapp2
  webapp1:
    build: ./webapp
    hostname: webapp1                                                #3
  webapp2:
    build: ./webapp
    hostname: webapp2                                                #3


  1. Use the previous configuration file
  2. Only expose the API Gateway to the outside world
  3. Set the hostname to display it on the page

We can log in using one of the two hard-coded accounts. I'm using john, with password john and label John Doe. Notice that Apache APISIX sets me on a node and keeps using the same if I refresh.

We can try to log in with the other account (jane/jane) from a private window and check the counter starts from 0.

Now comes the fun part. Let's stop the node which should be hosting the session data, here webapp2 and refresh the page:

Shell
 
docker compose stop webapp2


We can see exciting things in the logs. Apache APISIX can no longer find the webapp2, so it forwards the request to the other upstream that it knows, webapp1.

  1. The request is still authenticated; it goes through Spring Security
  2. The framework gets the principal out of the request
  3. It queries Spring Session
  4. And gets the correct counter value that Hazelcast replicated from the other node

The only side-effect is an increased latency because of Apache APISIX timeout. It's 5 seconds by default, but you can configure it to a lower value if needed.

When we start webapp2 again, everything works as expected again.

Conclusion

In this post, I described a possible setup for sticky sessions with Apache APISIX and replication involving the Spring ecosystem and Hazelcast. Many other options are available depending on your stack and framework of choices.

The complete source code for this post can be found on GitHub.

To go further:

  • Spring Session and Spring Security with Hazelcast
  • Spring Session Hazelcast
Hazelcast Java virtual machine Spring Security Spring Boot

Published at DZone with permission of Nicolas Fränkel, DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Is Spring AI Strong Enough for AI?
  • Comparing the Efficiency of a Spring Boot Project to a Go Project
  • Shift-Left Monitoring Approach for Cloud Apps in Containers
  • Basic Authentication Using Spring Boot Security: A Step-By-Step Guide

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!