Distributed Session Management with Spring Boot and Spring Session

Introduction

Managing sessions in a distributed environment is a common challenge in modern web applications. Spring Session provides a robust solution to this problem, allowing session data to be stored in different types of persistent storage, such as relational databases, Redis, and Hazelcast. This article explores the theoretical components of Spring Session, practical implementation examples, and a real-world use case.

What is a Spring Session?

Spring Session is a Spring project that provides an API and implementations for managing user-session information. It replaces the standard HttpSession with a more robust implementation that allows session persistence in various types of storage, such as Redis, JDBC (relational databases), and Hazelcast. The main goal of Spring Session is to solve common problems in distributed environments, such as data consistency and scalability.

Key Attributes of the Spring Session Class
  • sessionId: Unique identifier of the session.

  • creationTime: Timestamp indicating when the session was created.

  • lastAccessedTime: Timestamp indicating the last time the session was accessed.

  • maxInactiveInterval: Maximum inactivity interval before the session is invalidated.

  • attributes: Map containing session attributes.

What is an HttpSession?

HttpSession is an interface in the Java Servlet API that allows storing session information between different HTTP requests. The session is created by the web server when a user first accesses a web application and lasts until it is explicitly ended or expires due to inactivity.

Theoretical Components of Spring Session

  • Spring Session Core:

    • Provides APIs and implementations for managing user session information.

    • Supports HttpSession, WebSession, and WebSocket.

  • Spring Session Data Redis:

    • Session repository implementation for managing sessions in Redis.

    • Redis is used for its speed and efficiency in handling in-memory data.

  • Spring Session JDBC:

    • Session repository implementation for relational databases like MySQL.

    • Allows session persistence in a relational database, facilitating backup and recovery.

  • Spring Session Hazelcast:

    • Session repository implementation for Hazelcast.

    • Hazelcast is a distributed in-memory data grid that supports scalability and high availability.

  • Transparent Integration with Spring Boot:

    • Spring Boot’s auto-configuration allows effortless integration with Spring Session.

    • Use annotations like @EnableRedisHttpSession to enable session management with Redis.

Simple Example 1: Spring Session with Redis

Project Configuration

Adding Dependencies in pom.xml:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  • spring-session-data-redis: Dependency required to integrate Spring Session with Redis.

  • spring-boot-starter-data-redis: Spring Boot starter that simplifies Redis configuration.

Redis Configuration in application.properties:

spring.session.store-type=redis
spring.redis.host=localhost
spring.redis.port=6379

Main Configuration Class:

@SpringBootApplication
public class SpringBootSessionApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSessionApplication.class, args);
    }
}
  • @SpringBootApplication: Annotation that marks the main class of a Spring Boot application.

  • SpringApplication.run: Method to start the Spring Boot application.

Detailed Explanation of the Session Controller Code

The code below is a Spring MVC controller that handles HTTP sessions to store and retrieve user messages. Let's break down line by line what each part of the code does.

@Controller
public class SessionController {
    @GetMapping("/")
    public String home(HttpSession session, Model model) {
        List<String> messages = (List<String>) session.getAttribute("MY_SESSION_MESSAGES");
        if (messages == null) {
            messages = new ArrayList<>();
        }
        model.addAttribute("sessionMessages", messages);
        return "index";
    }

    @PostMapping("/message")
    public String saveMessage(@RequestParam("message") String message, HttpSession session) {
        List<String> messages = (List<String>) session.getAttribute("MY_SESSION_MESSAGES");
        if (messages == null) {
            messages = new ArrayList<>();
            session.setAttribute("MY_SESSION_MESSAGES", messages);
        }
        messages.add(message);
        session.setAttribute("MY_SESSION_MESSAGES", messages);
        return "redirect:/";
    }
}

Detailed Explanation

  1. @Controller Annotation:

    • Indicates that the SessionController class is a Spring MVC controller, responsible for handling HTTP requests.
  2. home Method:

     @GetMapping("/")
     public String home(HttpSession session, Model model) {
         List<String> messages = (List<String>) session.getAttribute("MY_SESSION_MESSAGES");
         if (messages == null) {
             messages = new ArrayList<>();
         }
         model.addAttribute("sessionMessages", messages);
         return "index";
     }
    
    • @GetMapping("/"): Maps HTTP GET requests to the root path ("/") to the home method.

    • HttpSession session: Injects the current HTTP session into the method.

    • Model model: Injects the model to be passed to the view.

    • session.getAttribute("MY_SESSION_MESSAGES"): Attempts to retrieve the list of messages stored in the session under the key "MY_SESSION_MESSAGES".

    • if (messages == null) { messages = new ArrayList<>(); }: If the list of messages does not exist in the session (is null), initializes a new empty list.

    • model.addAttribute("sessionMessages", messages);: Adds the list of messages to the model with the key "sessionMessages" to be used in the view.

    • return "index";: Returns the view name "index" for rendering.

  3. saveMessage Method:

     @PostMapping("/message")
     public String saveMessage(@RequestParam("message") String message, HttpSession session) {
         List<String> messages = (List<String>) session.getAttribute("MY_SESSION_MESSAGES");
         if (messages == null) {
             messages = new ArrayList<>();
             session.setAttribute("MY_SESSION_MESSAGES", messages);
         }
         messages.add(message);
         session.setAttribute("MY_SESSION_MESSAGES", messages);
         return "redirect:/";
     }
    
    • @PostMapping("/message"): Maps HTTP POST requests to the path /message to the saveMessage method.

    • @RequestParam("message") String message: Injects the message parameter from the HTTP request into the method.

    • HttpSession session: Injects the current HTTP session into the method.

    • session.getAttribute("MY_SESSION_MESSAGES"): Attempts to retrieve the list of messages stored in the session under the key "MY_SESSION_MESSAGES".

    • if (messages == null) { messages = new ArrayList<>(); session.setAttribute("MY_SESSION_MESSAGES", messages); }: If the list of messages does not exist in the session (is null), initializes a new empty list and stores it in the session under the key "MY_SESSION_MESSAGES".

    • messages.add(message);: Adds the new message to the list.

    • session.setAttribute("MY_SESSION_MESSAGES", messages);: Updates the list of messages in the session under the key "MY_SESSION_MESSAGES".

    • return "redirect:/";: Redirects the request to the root path ("/"), triggering the home method again to display the updated list of messages.

Key Points

  • MY_SESSION_MESSAGES: This is the key used to store and retrieve the list of messages in the HTTP session. Initially, this key may not exist in the session, which is why the code checks if the returned value is null and initializes a new list if necessary.

  • session.getAttribute and session.setAttribute: Methods used to manipulate session attributes. They allow storing and retrieving data that persists across multiple HTTP requests from the same user.

  • Redirect (redirect:/): After saving a new message, the application redirects the user to the root path, where the list of messages is retrieved and displayed in the "index" view.

Simple Example 2: Spring Session with JDBC

Project Configuration

Adding Dependencies in pom.xml:

<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
  • spring-session-jdbc: Dependency to integrate Spring Session with JDBC.

  • mysql-connector-java: JDBC driver for MySQL.

Database Connection Configuration in application.properties:

spring.datasource.url=jdbc:mysql://localhost:3306/spring_session
spring.datasource.username=root
spring.datasource.password=secret
spring.session.store-type=jdbc
  • spring.datasource.url: URL for connecting to the MySQL database.

  • spring.datasource.username: Username for database access.

  • spring.datasource.password: Password for database access.

  • spring.session.store-type: Defines the session storage type as JDBC.

Creating the Session Table in MySQL:

CREATE TABLE SPRING_SESSION (
    PRIMARY_ID CHAR(36) NOT NULL,
    SESSION_ID CHAR(36) NOT NULL,
    CREATION_TIME BIGINT NOT NULL,
    LAST_ACCESS_TIME BIGINT NOT NULL,
    MAX_INACTIVE_INTERVAL INT NOT NULL,
    EXPIRY_TIME BIGINT NOT NULL,
    PRINCIPAL_NAME VARCHAR(100),
    PRIMARY KEY (PRIMARY_ID)
) ENGINE=InnoDB;

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);
  • Creates the SPRING_SESSION table to store session data.

  • Indexes to improve performance for searching by SESSION_ID, EXPIRY_TIME, and PRINCIPAL_NAME.

Reusing Main Configuration Class and Controller:

  • The main class and controller from the Redis example can be reused without changes.

Complex Example: Session Management with Spring Security

Project Configuration

Adding Dependencies in pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.session</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

Spring Security Configuration in SecurityConfig.java:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().permitAll()
            .and()
            .logout().permitAll();
    }
}
  • @EnableWebSecurity: Enables web security configuration.

  • HttpSecurity: Configures HTTP security, allowing public access to the root ("/") and requiring authentication for all other requests.

Reusing Redis Connection Configuration in application.properties:

  • The Redis configuration is the same as in the simple example.

Reusing Main Configuration Class and Controllers:

  • The main class and controllers can be reused, adding security logic as needed.

Real-World Use Case: E-commerce Application with Spring Session and Redis

A distributed e-commerce application needs to keep user sessions consistent across different server instances, ensuring that shopping cart items and authentication states are preserved even if the user is redirected between servers during navigation.

Implementation

Project Configuration:

  • Use the configuration from the simple Redis example.

Shopping Cart Controller:

@Controller
@RequestMapping("/cart")
public class CartController {
    @GetMapping
    public String showCart(HttpSession session, Model model) {
        Cart cart = (Cart) session.getAttribute("cart");
        if (cart == null) {
            cart = new Cart();
            session.setAttribute("cart", cart);
        }
        model.addAttribute("cart", cart);
        return "cart";
    }

    @PostMapping("/add")
    public String addItemToCart(@RequestParam("productId") Long productId, HttpSession session) {
        Cart cart = (Cart) session.getAttribute("cart");
        if (cart == null) {
            cart = new Cart();
            session.setAttribute("cart", cart);
        }
        cart.addItem(productId);
        return "redirect:/cart";
    }
}

Cart Model:

public class Cart {
    private List<Long> items = new ArrayList<>();

    public void addItem(Long productId) {
        items.add(productId);
    }

    public List<Long> getItems() {
        return items;
    }
}

Redis Configuration:

  • Ensure that Redis is correctly configured and running as per the simple example instructions.

Conclusion

Spring Session simplifies distributed session management in Spring Boot applications, providing seamless integration with various types of persistent storage. By using Redis, JDBC, or Hazelcast, developers can ensure that user sessions are consistent and persistent in distributed environments, improving application scalability and resilience.

0
Subscribe to my newsletter

Read articles from André Felipe Costa Bento directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

André Felipe Costa Bento
André Felipe Costa Bento

Fullstack Software Engineer.