This version is still in development and is not considered stable yet. For the latest stable version, please use Spring Session 3.4.1! |
Redis Configurations
Now that you have your application configured, you might want to start customizing things:
-
I want to customize the Redis configuration using Spring Boot properties
-
I want help in choosing
RedisSessionRepository
orRedisIndexedSessionRepository
. -
I want to serialize the session using JSON.
-
I want to specify a different namespace.
-
I want to know when a session is created, deleted, destroyed or expires.
-
I want to find all sessions of a specific user
-
I want to safe deserialize Redis sessions
-
Customizing the session expiration store
Serializing the Session using JSON
By default, Spring Session uses Java Serialization to serialize the session attributes.
Sometimes it might be problematic, especially when you have multiple applications that use the same Redis instance but have different versions of the same class.
You can provide a RedisSerializer
bean to customize how the session is serialized into Redis.
Spring Data Redis provides the GenericJackson2JsonRedisSerializer
that serializes and deserializes objects using Jackson’s ObjectMapper
.
@Configuration
public class SessionConfig implements BeanClassLoaderAware {
private ClassLoader loader;
/**
* Note that the bean name for this bean is intentionally
* {@code springSessionDefaultRedisSerializer}. It must be named this way to override
* the default {@link RedisSerializer} used by Spring Session.
*/
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericJackson2JsonRedisSerializer(objectMapper());
}
/**
* Customized {@link ObjectMapper} to add mix-in for class that doesn't have default
* constructors
* @return the {@link ObjectMapper} to use
*/
private ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModules(SecurityJackson2Modules.getModules(this.loader));
return mapper;
}
/*
* @see
* org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang
* .ClassLoader)
*/
@Override
public void setBeanClassLoader(ClassLoader classLoader) {
this.loader = classLoader;
}
}
The above code snippet is using Spring Security, therefore we are creating a custom ObjectMapper
that uses Spring Security’s Jackson modules.
If you do not need Spring Security Jackson modules, you can inject your application’s ObjectMapper
bean and use it like so:
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer(ObjectMapper objectMapper) {
return new GenericJackson2JsonRedisSerializer(objectMapper);
}
Specifying a Different Namespace
It is not uncommon to have multiple applications that use the same Redis instance.
For that reason, Spring Session uses a namespace
(defaults to spring:session
) to keep the session data separated if needed.
Using Spring Boot Properties
You can specify it by setting the spring.session.redis.namespace
property.
spring.session.redis.namespace=spring:session:myapplication
spring:
session:
redis:
namespace: "spring:session:myapplication"
Using the Annotation’s Attributes
You can specify the namespace
by setting the redisNamespace
property in the @EnableRedisHttpSession
, @EnableRedisIndexedHttpSession
, or @EnableRedisWebSession
annotations:
@Configuration
@EnableRedisHttpSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
// ...
}
@Configuration
@EnableRedisIndexedHttpSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
// ...
}
@Configuration
@EnableRedisWebSession(redisNamespace = "spring:session:myapplication")
public class SessionConfig {
// ...
}
Choosing Between RedisSessionRepository
and RedisIndexedSessionRepository
When working with Spring Session Redis, you will likely have to choose between the RedisSessionRepository
and the RedisIndexedSessionRepository
.
Both are implementations of the SessionRepository
interface that store session data in Redis.
However, they differ in how they handle session indexing and querying.
-
RedisSessionRepository
:RedisSessionRepository
is a basic implementation that stores session data in Redis without any additional indexing. It uses a simple key-value structure to store session attributes. Each session is assigned a unique session ID, and the session data is stored under a Redis key associated with that ID. When a session needs to be retrieved, the repository queries Redis using the session ID to fetch the associated session data. Since there is no indexing, querying sessions based on attributes or criteria other than the session ID can be inefficient. -
RedisIndexedSessionRepository
:RedisIndexedSessionRepository
is an extended implementation that provides indexing capabilities for sessions stored in Redis. It introduces additional data structures in Redis to efficiently query sessions based on attributes or criteria. In addition to the key-value structure used byRedisSessionRepository
, it maintains additional indexes to enable fast lookups. For example, it may create indexes based on session attributes like user ID or last access time. These indexes allow for efficient querying of sessions based on specific criteria, enhancing performance and enabling advanced session management features. In addition to that,RedisIndexedSessionRepository
also supports session expiration and deletion.
When using RedisIndexedSessionRepository with Redis Cluster you must be aware that it only subscribe to events from one random redis node in the cluster, which can cause some session indexes not being cleaned up if the event happened in a different node.
|
Configuring the RedisSessionRepository
Using Spring Boot Properties
If you are using Spring Boot, the RedisSessionRepository
is the default implementation.
However, if you want to be explicit about it, you can set the following property in your application:
spring.session.redis.repository-type=default
spring:
session:
redis:
repository-type: default
Configuring the RedisIndexedSessionRepository
Listening to Session Events
Often times it is valuable to react to session events, for example, you might want to do some kind of processing depending on the session lifecycle. In order to be able to do that, you must be using the indexed repository. If you do not know the difference between the indexed and the default repository, you can go to this section.
With the indexed repository configured, you can now start to listen to SessionCreatedEvent
, SessionDeletedEvent
, SessionDestroyedEvent
and SessionExpiredEvent
events.
There are a few ways to listen to application events in Spring, we are going to use the @EventListener
annotation.
@Component
public class SessionEventListener {
@EventListener
public void processSessionCreatedEvent(SessionCreatedEvent event) {
// do the necessary work
}
@EventListener
public void processSessionDeletedEvent(SessionDeletedEvent event) {
// do the necessary work
}
@EventListener
public void processSessionDestroyedEvent(SessionDestroyedEvent event) {
// do the necessary work
}
@EventListener
public void processSessionExpiredEvent(SessionExpiredEvent event) {
// do the necessary work
}
}
Finding All Sessions of a Specific User
By retrieving all sessions of a specific user, you can track the user’s active sessions across devices or browsers. For example, you can use this information session management purposes, such as allowing the user to invalidate or logout from specific sessions or performing actions based on the user’s session activity.
To do that, first you must be using the indexed repository, and then you can inject the FindByIndexNameSessionRepository
interface, like so:
@Autowired
public FindByIndexNameSessionRepository<? extends Session> sessions;
public Collection<? extends Session> getSessions(Principal principal) {
Collection<? extends Session> usersSessions = this.sessions.findByPrincipalName(principal.getName()).values();
return usersSessions;
}
public void removeSession(Principal principal, String sessionIdToDelete) {
Set<String> usersSessionIds = this.sessions.findByPrincipalName(principal.getName()).keySet();
if (usersSessionIds.contains(sessionIdToDelete)) {
this.sessions.deleteById(sessionIdToDelete);
}
}
In the example above, you can use the getSessions
method to find all sessions of a specific user, and the removeSession
method to remove a specific session of a user.
Configuring Redis Session Mapper
Spring Session Redis retrieves session information from Redis and stores it in a Map<String, Object>
.
This map needs to undergo a mapping process to be transformed into a MapSession
object, which is then utilized within RedisSession
.
The default mapper used for this purpose is called RedisSessionMapper
.
If the session map doesn’t contain the minimum necessary keys to construct the session, like creationTime
, this mapper will throw an exception.
One possible scenario for the absence of required keys is when the session key is deleted concurrently, usually due to expiration, while the save process is in progress.
This occurs because the HSET command is employed to set fields within the key, and if the key doesn’t exist, this command will create it.
If you want to customize the mapping process, you can create your implementation of BiFunction<String, Map<String, Object>, MapSession>
and set it into the session repository.
The following example shows how to delegate the mapping process to the default mapper, but if an exception is thrown, the session is deleted from Redis:
-
RedisSessionRepository
-
RedisIndexedSessionRepository
-
ReactiveRedisSessionRepository
@Configuration
@EnableRedisHttpSession
public class SessionConfig {
@Bean
SessionRepositoryCustomizer<RedisSessionRepository> redisSessionRepositoryCustomizer() {
return (redisSessionRepository) -> redisSessionRepository
.setRedisSessionMapper(new SafeRedisSessionMapper(redisSessionRepository));
}
static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, MapSession> {
private final RedisSessionMapper delegate = new RedisSessionMapper();
private final RedisSessionRepository sessionRepository;
SafeRedisSessionMapper(RedisSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public MapSession apply(String sessionId, Map<String, Object> map) {
try {
return this.delegate.apply(sessionId, map);
}
catch (IllegalStateException ex) {
this.sessionRepository.deleteById(sessionId);
return null;
}
}
}
}
@Configuration
@EnableRedisIndexedHttpSession
public class SessionConfig {
@Bean
SessionRepositoryCustomizer<RedisIndexedSessionRepository> redisSessionRepositoryCustomizer() {
return (redisSessionRepository) -> redisSessionRepository.setRedisSessionMapper(
new SafeRedisSessionMapper(redisSessionRepository.getSessionRedisOperations()));
}
static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, MapSession> {
private final RedisSessionMapper delegate = new RedisSessionMapper();
private final RedisOperations<String, Object> redisOperations;
SafeRedisSessionMapper(RedisOperations<String, Object> redisOperations) {
this.redisOperations = redisOperations;
}
@Override
public MapSession apply(String sessionId, Map<String, Object> map) {
try {
return this.delegate.apply(sessionId, map);
}
catch (IllegalStateException ex) {
// if you use a different redis namespace, change the key accordingly
this.redisOperations.delete("spring:session:sessions:" + sessionId); // we do not invoke RedisIndexedSessionRepository#deleteById to avoid an infinite loop because the method also invokes this mapper
return null;
}
}
}
}
@Configuration
@EnableRedisWebSession
public class SessionConfig {
@Bean
ReactiveSessionRepositoryCustomizer<ReactiveRedisSessionRepository> redisSessionRepositoryCustomizer() {
return (redisSessionRepository) -> redisSessionRepository
.setRedisSessionMapper(new SafeRedisSessionMapper(redisSessionRepository));
}
static class SafeRedisSessionMapper implements BiFunction<String, Map<String, Object>, Mono<MapSession>> {
private final RedisSessionMapper delegate = new RedisSessionMapper();
private final ReactiveRedisSessionRepository sessionRepository;
SafeRedisSessionMapper(ReactiveRedisSessionRepository sessionRepository) {
this.sessionRepository = sessionRepository;
}
@Override
public Mono<MapSession> apply(String sessionId, Map<String, Object> map) {
return Mono.fromSupplier(() -> this.delegate.apply(sessionId, map))
.onErrorResume(IllegalStateException.class,
(ex) -> this.sessionRepository.deleteById(sessionId).then(Mono.empty()));
}
}
}
Customizing the Session Expiration Store
Due to the nature of Redis, there is no guarantee on when an expired event will be fired if the key has not been accessed. For more details, refer to the Redis documentation on key expiration.
To mitigate the uncertainty of expired events, sessions are also stored with their expected expiration times.
This ensures that each key can be accessed when it is expected to expire.
The RedisSessionExpirationStore
interface defines the common operations for tracking sessions and their expiration times, and it provides a strategy for cleaning up expired sessions.
By default, each session expiration is tracked to the nearest minute. This allows a background task to access the potentially expired sessions to ensure that Redis expired events are fired in a more deterministic fashion.
For example:
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations:1439245080000 2100
The background task will then use these mappings to explicitly request each session expires key. By accessing the key, rather than deleting it, we ensure that Redis deletes the key for us only if the TTL is expired.
By customizing the session expiration store, you can manage session expiration more effectively based on your needs.
To do that, you should provide a bean of type RedisSessionExpirationStore
that will be picked up by Spring Session Data Redis configuration:
-
SessionConfig
import org.springframework.session.data.redis.SortedSetRedisSessionExpirationStore;
@Configuration
@EnableRedisIndexedHttpSession
public class SessionConfig {
@Bean
public RedisSessionExpirationStore redisSessionExpirationStore(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return new SortedSetRedisSessionExpirationStore(redisTemplate, RedisIndexedSessionRepository.DEFAULT_NAMESPACE);
}
}
In the code above, the SortedSetRedisSessionExpirationStore
implementation is being used, which uses a Sorted Set to store the session ids with their expiration time as the score.
We do not explicitly delete the keys since in some instances there may be a race condition that incorrectly identifies a key as expired when it is not. Short of using distributed locks (which would kill performance) there is no way to ensure the consistency of the expiration mapping. By simply accessing the key, we ensure that the key is only removed if the TTL on that key is expired. However, for your implementations you can choose the strategy that best fits. |