Skip to content
Ashish.
All posts
Diagram illustrating Spring Session architecture with Redis as the central session store.

Spring Session: Distributed Session Management

An overview of Spring Session for distributed session management using Redis and session clustering.

By Ashish SrivastavaPart 12 of Spring Security Deep Dive Series

In a standard monolithic Java application, the HttpSession object resides in the JVM heap. When you add a user to the session, the data stays local. If you scale horizontally by adding a second server, a user logging in on Server A finds their session data missing on Server B. Traditional clustering solves this via session replication, but this creates significant network traffic and latency.

Spring Session solves this by removing the dependency on the container's internal session management. Instead of storing session data in the JVM, Spring Session implements the HttpSession interface using a "Session Repository." This repository acts as a bridge, translating standard Servlet API calls into operations against an external data store, such as Redis. The application remains unaware of the distribution; it simply calls session.setAttribute(), and Spring Session handles the serialization and storage elsewhere.

This article is Part 12 of the Spring Security Deep Dive Series.

The Interceptor Mechanism

The core mechanism enabling this decoupling is the SessionRepositoryFilter. This filter is registered in the servlet chain before your application's controllers execute. When a request arrives, the filter intercepts the HttpServletRequest. It checks for a session ID cookie (e.g., SESSIONID). If the ID exists, the filter queries the configured Session Repository (Redis) to load the session data.

Consider an actor named Alice making a request. Her browser sends a cookie SESSIONID=abc-123. The SessionRepositoryFilter extracts abc-123 and calls sessionRepository.findById("abc-123"). Redis returns the serialized data, which the filter deserializes into a SpringSession object. This object is then attached to the request attributes, making it available to your code as if it were a local variable.

When the request completes, the filter inspects the SpringSession object. If the application called session.setAttribute("user", "Alice"), the filter marks the session as "dirty." At the end of the request, the filter serializes the updated session data and writes it back to Redis. If the session was accessed but not modified, it may still be touched to update the last-accessed timestamp, depending on the configuration. This entire flow happens before your controller logic sees the request, ensuring that every request, regardless of which server instance handles it, sees the same session state.

Redis as the Backend Store

Redis is the preferred backing store for Spring Session due to its speed and native support for expiration. Spring Session maps the HttpSession concept directly to Redis data structures. Specifically, it uses Redis Hashes to store session attributes.

A single session corresponds to a single Redis key, typically named spring:session:data:{session-id}. The attributes within the session are stored as fields within that hash. For example, the attribute user with value Alice becomes a field user with value Alice inside the hash.

The lifecycle of a session in Redis involves specific commands:

  1. Creation: When a new session is created, Spring Session issues an HSET command to create the hash and an EXPIRE command to set the TTL (Time To Live) based on the session timeout configuration.
  2. Read/Write: Reads use HGET or HMGET. Writes use HSET.
  3. Cleanup: Redis automatically removes keys that exceed their TTL. Spring Session also relies on a separate "cleanup" mechanism that scans for expired sessions if the store does not handle all cleanup natively, though Redis expiration handles the bulk of this work efficiently.

This structure allows multiple application instances to read and write to the same logical session without locking contention, provided the Redis server handles the concurrency correctly.

Serialization and Security Tradeoffs

How data moves between the JVM and Redis is critical. By default, Spring Session uses Java's built-in JDKSerializationStrategy. This strategy converts objects into a byte stream that can be reconstructed later. While convenient, this approach has two major downsides.

First, security. If an attacker gains access to the Redis instance or the network traffic, they can manipulate the serialized bytes. Upon deserialization, this could lead to Remote Code Execution (RCE) if the attacker can inject malicious objects into the stream. This is a known vulnerability in many Java frameworks that rely on default serialization.

Second, interoperability. Serialized Java objects are binary and tied to the specific class versions used during serialization. If you update your application classes (e.g., changing a field name), deserialization might fail on other nodes unless you manage versioning carefully.

The recommended approach is to use GenericJackson2JsonRedisSerializer. This serializer converts session attributes into JSON strings before storing them in Redis. JSON is human-readable, language-agnostic, and safer because it does not deserialize arbitrary Java objects by default.

import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.data.redis.RedisSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.data.redis.config.annotation.web.http.RedisSessionRepositoryFilter;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.StringRedisSerializer;
 
import java.time.Duration;
 
@Configuration
public class SessionConfig {
 
    @Bean
    public SessionRepositoryFilter<? extends Session> sessionRepositoryFilter(
            RedisSessionRepository redisSessionRepository) {
        return new SessionRepositoryFilter<>(redisSessionRepository);
    }
 
    @Bean
    public RedisSessionRepository redisSessionRepository(
            RedisConnectionFactory redisConnectionFactory) {
        RedisSessionRepository repository = new RedisSessionRepository(redisConnectionFactory);
        
        // Configure JSON serialization to avoid JDK serialization risks
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
        repository.setSessionSerializer(serializer);
        repository.setSessionIdSerializer(new StringRedisSerializer());
        
        // Set the default timeout (e.g., 30 minutes)
        repository.setDefaultMaxInactiveInterval(Duration.ofMinutes(30));
        
        return repository;
    }
}

Using JSON serialization ensures that even if you change your application's internal class structure, the session data remains readable, provided the JSON mapping logic handles the evolution gracefully. It also allows you to inspect session data directly in Redis using redis-cli commands like HGETALL spring:session:data:abc-123 without needing to deserialize Java objects locally.

Configuration and Scaling

To implement this, you must exclude the default Tomcat session manager and ensure Spring Boot auto-configures the Redis connection. In your application.properties, you enable the Redis module and define the connection string.

spring.session.store-type=redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
server.servlet.session.timeout=30m
server.servlet.session.cookie.http-only=true
server.servlet.session.cookie.secure=true

The spring.session.store-type=redis property triggers the auto-configuration of the SessionRepositoryFilter and the RedisSessionRepository. Without this, Spring Boot defaults to COOKIE store (in-memory) or JDBC, depending on the presence of a database.

When scaling your application, you simply add more instances. All instances connect to the same Redis cluster. If Instance 1 creates a session, Instance 2 can immediately retrieve it because they share the same Redis backend. There is no need for complex multicast protocols or network topology configurations. The state is externalized.

However, this introduces a new dependency: Redis availability. If Redis goes down, your session management fails. You must ensure Redis is deployed with high availability (e.g., Redis Sentinel or Cluster mode) to prevent a single point of failure from taking down your user authentication. Additionally, the latency of the network round-trip to Redis adds to the request processing time. For most applications, this latency is negligible compared to database queries or external API calls, but in high-frequency trading or ultra-low-latency systems, the overhead of the network hop must be measured.

Conclusion

Spring Session transforms session management from a container-level concern into an application-level architectural decision. By replacing the in-memory HttpSession with a SessionRepository, it enables stateless application servers to share state through a robust, scalable backend like Redis. The mechanism relies on a filter to intercept requests, serialize attributes into a safe format (preferably JSON), and persist them to a shared store. This approach eliminates the complexity of session replication, reduces memory pressure on individual nodes, and simplifies horizontal scaling. The tradeoff is the reliance on an external store and the need to manage serialization safely, but for modern microservices and clustered deployments, these are manageable costs for the gain in flexibility.

Common Pitfalls

  1. Redis Downtime: If Redis becomes unavailable, all active sessions are lost, and users will be forced to re-authenticate. Ensure you have a robust monitoring strategy and failover mechanism.
  2. Serialization Versioning: Even with JSON, changing the structure of session attributes (e.g., renaming fields) can break deserialization if not handled with backward-compatible mapping strategies.
  3. Network Latency: Every session read and write requires a network round-trip to Redis. In high-throughput systems, this added latency can become a bottleneck if the Redis cluster is not co-located or optimized.

Practical Takeaways

  • Externalize State: Treat your session store as a critical dependency, not just a convenience. Design your infrastructure to handle Redis failures gracefully.
  • Prefer JSON: Always use GenericJackson2JsonRedisSerializer or similar JSON-based strategies to avoid RCE vulnerabilities and improve cross-language compatibility.
  • Monitor TTLs: Keep a close eye on session timeouts and Redis memory usage to prevent accidental data loss or resource exhaustion.

FAQ

Q: Can I use Spring Session with a database other than Redis? A: Yes, Spring Session supports JDBC and Hazelcast stores, though Redis is generally preferred for performance in distributed environments.

Q: Does Spring Session work with Spring Cloud Gateway? A: Yes, provided you configure the gateway to pass session cookies correctly and ensure the backend services share the same Redis store.

Q: How does Spring Session handle session fixation attacks? A: Spring Session does not automatically fix session fixation. You must implement your own logic to regenerate the session ID upon authentication using session.fixate().

Q: Is there a performance penalty compared to in-memory sessions? A: Yes, there is a latency cost due to network I/O, but for most web applications, the benefit of horizontal scaling outweighs the minor increase in request time.

Related posts