Skip to content

Commit 5d0775b

Browse files
committed
Ensure RedisHttpSessionConfiguration handles events for configured database
At present, RedisHttpSessionConfiguration doesn't take into account database index when handlng events. In situations where multiple apps use Spring Session with same Redis instance, but different database, this results in invalid session events. This commits improves event handling in RedisHttpSessionConfiguration to ensure currently used database is considered. Closes gh-1128
1 parent 603a258 commit 5d0775b

File tree

4 files changed

+170
-40
lines changed

4 files changed

+170
-40
lines changed

spring-session-data-redis/src/integration-test/java/org/springframework/session/data/redis/RedisOperationsSessionRepositoryITests.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.session.data.redis;
1818

19+
import java.nio.charset.StandardCharsets;
1920
import java.util.Map;
2021
import java.util.UUID;
2122

@@ -190,9 +191,10 @@ public void findByPrincipalNameExpireRemovesIndex() throws Exception {
190191

191192
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:"
192193
+ toSave.getId();
193-
String channel = ":expired";
194-
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"),
195-
body.getBytes("UTF-8"));
194+
String channel = "__keyevent@0__:expired";
195+
DefaultMessage message = new DefaultMessage(
196+
channel.getBytes(StandardCharsets.UTF_8),
197+
body.getBytes(StandardCharsets.UTF_8));
196198
byte[] pattern = new byte[] {};
197199
this.repository.onMessage(message, pattern);
198200

@@ -358,9 +360,10 @@ public void findBySecurityPrincipalNameExpireRemovesIndex() throws Exception {
358360

359361
String body = "RedisOperationsSessionRepositoryITests:sessions:expires:"
360362
+ toSave.getId();
361-
String channel = ":expired";
362-
DefaultMessage message = new DefaultMessage(channel.getBytes("UTF-8"),
363-
body.getBytes("UTF-8"));
363+
String channel = "__keyevent@0__:expired";
364+
DefaultMessage message = new DefaultMessage(
365+
channel.getBytes(StandardCharsets.UTF_8),
366+
body.getBytes(StandardCharsets.UTF_8));
364367
byte[] pattern = new byte[] {};
365368
this.repository.onMessage(message, pattern);
366369

spring-session-data-redis/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ public class RedisOperationsSessionRepository implements
254254

255255
static PrincipalNameResolver PRINCIPAL_NAME_RESOLVER = new PrincipalNameResolver();
256256

257+
/**
258+
* The default Redis database used by Spring Session.
259+
*/
260+
public static final int DEFAULT_DATABASE = 0;
261+
257262
/**
258263
* The default namespace for each key and channel in Redis used by Spring Session.
259264
*/
@@ -286,11 +291,19 @@ public class RedisOperationsSessionRepository implements
286291
*/
287292
static final String SESSION_ATTR_PREFIX = "sessionAttr:";
288293

294+
private int database = RedisOperationsSessionRepository.DEFAULT_DATABASE;
295+
289296
/**
290297
* The namespace for every key used by Spring Session in Redis.
291298
*/
292299
private String namespace = DEFAULT_NAMESPACE + ":";
293300

301+
private String sessionCreatedChannelPrefix;
302+
303+
private String sessionDeletedChannel;
304+
305+
private String sessionExpiredChannel;
306+
294307
private final RedisOperations<Object, Object> sessionRedisOperations;
295308

296309
private final RedisSessionExpirationPolicy expirationPolicy;
@@ -327,6 +340,7 @@ public RedisOperationsSessionRepository(
327340
this.sessionRedisOperations = sessionRedisOperations;
328341
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations,
329342
this::getExpirationsKey, this::getSessionKey);
343+
configureSessionChannels();
330344
}
331345

332346
/**
@@ -377,6 +391,18 @@ public void setRedisFlushMode(RedisFlushMode redisFlushMode) {
377391
this.redisFlushMode = redisFlushMode;
378392
}
379393

394+
public void setDatabase(int database) {
395+
this.database = database;
396+
configureSessionChannels();
397+
}
398+
399+
private void configureSessionChannels() {
400+
this.sessionCreatedChannelPrefix = this.namespace + "event:" + this.database
401+
+ ":created:";
402+
this.sessionDeletedChannel = "__keyevent@" + this.database + "__:del";
403+
this.sessionExpiredChannel = "__keyevent@" + this.database + "__:expired";
404+
}
405+
380406
/**
381407
* Returns the {@link RedisOperations} used for sessions.
382408
* @return the {@link RedisOperations} used for sessions
@@ -502,7 +528,7 @@ public void onMessage(Message message, byte[] pattern) {
502528

503529
String channel = new String(messageChannel);
504530

505-
if (channel.startsWith(getSessionCreatedChannelPrefix())) {
531+
if (channel.startsWith(this.sessionCreatedChannelPrefix)) {
506532
// TODO: is this thread safe?
507533
Map<Object, Object> loaded = (Map<Object, Object>) this.defaultSerializer
508534
.deserialize(message.getBody());
@@ -515,8 +541,8 @@ public void onMessage(Message message, byte[] pattern) {
515541
return;
516542
}
517543

518-
boolean isDeleted = channel.endsWith(":del");
519-
if (isDeleted || channel.endsWith(":expired")) {
544+
boolean isDeleted = (this.sessionDeletedChannel).equals(channel);
545+
if (isDeleted || (this.sessionExpiredChannel).equals(channel)) {
520546
int beginIndex = body.lastIndexOf(":") + 1;
521547
int endIndex = body.length();
522548
String sessionId = body.substring(beginIndex, endIndex);
@@ -579,6 +605,7 @@ private void publishEvent(ApplicationEvent event) {
579605
public void setRedisKeyNamespace(String namespace) {
580606
Assert.hasText(namespace, "namespace cannot be null or empty");
581607
this.namespace = namespace.trim() + ":";
608+
configureSessionChannels();
582609
}
583610

584611
/**
@@ -610,17 +637,33 @@ private String getSessionCreatedChannel(String sessionId) {
610637
}
611638

612639
private String getExpiredKeyPrefix() {
613-
return this.namespace + "sessions:" + "expires:";
640+
return this.namespace + "sessions:expires:";
614641
}
615642

616643
/**
617-
* Gets the prefix for the channel that SessionCreatedEvent are published to. The
618-
* suffix is the session id of the session that was created.
619-
*
620-
* @return the prefix for the channel that SessionCreatedEvent are published to
644+
* Gets the prefix for the channel that {@link SessionCreatedEvent}s are published to.
645+
* The suffix is the session id of the session that was created.
646+
* @return the prefix for the channel that {@link SessionCreatedEvent}s are published
647+
* to
621648
*/
622649
public String getSessionCreatedChannelPrefix() {
623-
return this.namespace + "event:created:";
650+
return this.sessionCreatedChannelPrefix;
651+
}
652+
653+
/**
654+
* Gets the name of the channel that {@link SessionDeletedEvent}s are published to.
655+
* @return the name for the channel that {@link SessionDeletedEvent}s are published to
656+
*/
657+
public String getSessionDeletedChannel() {
658+
return this.sessionDeletedChannel;
659+
}
660+
661+
/**
662+
* Gets the name of the channel that {@link SessionExpiredEvent}s are published to.
663+
* @return the name for the channel that {@link SessionExpiredEvent}s are published to
664+
*/
665+
public String getSessionExpiredChannel() {
666+
return this.sessionExpiredChannel;
624667
}
625668

626669
/**

spring-session-data-redis/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,10 @@
3737
import org.springframework.core.type.AnnotationMetadata;
3838
import org.springframework.data.redis.connection.RedisConnection;
3939
import org.springframework.data.redis.connection.RedisConnectionFactory;
40+
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
41+
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
4042
import org.springframework.data.redis.core.RedisTemplate;
43+
import org.springframework.data.redis.listener.ChannelTopic;
4144
import org.springframework.data.redis.listener.PatternTopic;
4245
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
4346
import org.springframework.data.redis.serializer.RedisSerializer;
@@ -54,6 +57,7 @@
5457
import org.springframework.session.data.redis.config.annotation.SpringSessionRedisConnectionFactory;
5558
import org.springframework.session.web.http.SessionRepositoryFilter;
5659
import org.springframework.util.Assert;
60+
import org.springframework.util.ClassUtils;
5761
import org.springframework.util.StringUtils;
5862
import org.springframework.util.StringValueResolver;
5963

@@ -115,6 +119,8 @@ public RedisOperationsSessionRepository sessionRepository() {
115119
sessionRepository.setRedisKeyNamespace(this.redisNamespace);
116120
}
117121
sessionRepository.setRedisFlushMode(this.redisFlushMode);
122+
int database = resolveDatabase();
123+
sessionRepository.setDatabase(database);
118124
return sessionRepository;
119125
}
120126

@@ -128,9 +134,9 @@ public RedisMessageListenerContainer redisMessageListenerContainer() {
128134
if (this.redisSubscriptionExecutor != null) {
129135
container.setSubscriptionExecutor(this.redisSubscriptionExecutor);
130136
}
131-
container.addMessageListener(sessionRepository(),
132-
Arrays.asList(new PatternTopic("__keyevent@*:del"),
133-
new PatternTopic("__keyevent@*:expired")));
137+
container.addMessageListener(sessionRepository(), Arrays.asList(
138+
new ChannelTopic(sessionRepository().getSessionDeletedChannel()),
139+
new ChannelTopic(sessionRepository().getSessionExpiredChannel())));
134140
container.addMessageListener(sessionRepository(),
135141
Collections.singletonList(new PatternTopic(
136142
sessionRepository().getSessionCreatedChannelPrefix() + "*")));
@@ -256,6 +262,18 @@ private RedisTemplate<Object, Object> createRedisTemplate() {
256262
return redisTemplate;
257263
}
258264

265+
private int resolveDatabase() {
266+
if (ClassUtils.isPresent("io.lettuce.core.RedisClient", null)
267+
&& this.redisConnectionFactory instanceof LettuceConnectionFactory) {
268+
return ((LettuceConnectionFactory) this.redisConnectionFactory).getDatabase();
269+
}
270+
if (ClassUtils.isPresent("redis.clients.jedis.Jedis", null)
271+
&& this.redisConnectionFactory instanceof JedisConnectionFactory) {
272+
return ((JedisConnectionFactory) this.redisConnectionFactory).getDatabase();
273+
}
274+
return RedisOperationsSessionRepository.DEFAULT_DATABASE;
275+
}
276+
259277
/**
260278
* Ensures that Redis is configured to send keyspace notifications. This is important
261279
* to ensure that expiration and deletion of sessions trigger SessionDestroyedEvents.

0 commit comments

Comments
 (0)