Ensure Redis Configured to Send Keyspace Notifications
Previously there was a possibility that Session to WebSocket mapping was leaked if keyspace notifications were not enabled in Redis. To resolve this the RedisHttpSessionConfiguration now ensures that Redis is configured to enable Keyspace notifications. Fixes gh-76 gh-81
This commit is contained in:
@@ -47,18 +47,22 @@ public class SessionMessageListener implements MessageListener {
|
||||
}
|
||||
|
||||
public void onMessage(Message message, byte[] pattern) {
|
||||
byte[] messageChannel = message.getChannel();
|
||||
byte[] messageBody = message.getBody();
|
||||
if(messageBody == null) {
|
||||
if(messageChannel == null || messageBody == null) {
|
||||
return;
|
||||
}
|
||||
String channel = new String(messageChannel);
|
||||
if(!(channel.endsWith(":del") || channel.endsWith(":expired"))) {
|
||||
return;
|
||||
}
|
||||
String body = new String(messageBody);
|
||||
if(!("del".equals(body) || "expired".equals(body))) {
|
||||
if(!body.startsWith("spring:session:sessions:")) {
|
||||
return;
|
||||
}
|
||||
String channel = new String(message.getChannel());
|
||||
int beginIndex = channel.lastIndexOf(":") + 1;
|
||||
int endIndex = channel.length();
|
||||
String sessionId = channel.substring(beginIndex, endIndex);
|
||||
int beginIndex = body.lastIndexOf(":") + 1;
|
||||
int endIndex = body.length();
|
||||
String sessionId = body.substring(beginIndex, endIndex);
|
||||
|
||||
publishEvent(new SessionDestroyedEvent(this, sessionId));
|
||||
}
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
*/
|
||||
package org.springframework.session.data.redis.config.annotation.web.http;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.beans.factory.BeanClassLoaderAware;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
@@ -27,11 +30,13 @@ import org.springframework.context.annotation.ImportAware;
|
||||
import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.annotation.AnnotationUtils;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.data.redis.connection.RedisConnection;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.listener.PatternTopic;
|
||||
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.session.ExpiringSession;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.data.redis.RedisOperationsSessionRepository;
|
||||
@@ -51,6 +56,7 @@ import org.springframework.util.ClassUtils;
|
||||
* @see EnableRedisHttpSession
|
||||
*/
|
||||
@Configuration
|
||||
@EnableScheduling
|
||||
public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoaderAware {
|
||||
|
||||
private ClassLoader beanClassLoader;
|
||||
@@ -68,7 +74,7 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
|
||||
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
|
||||
container.setConnectionFactory(connectionFactory);
|
||||
container.addMessageListener(redisSessionMessageListener(),
|
||||
new PatternTopic("__keyspace@0__:spring:session:sessions:*"));
|
||||
Arrays.asList(new PatternTopic("__keyevent@*:del"),new PatternTopic("__keyevent@*:expired")));
|
||||
return container;
|
||||
}
|
||||
|
||||
@@ -136,6 +142,55 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
|
||||
this.httpSessionStrategy = httpSessionStrategy;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public EnableRedisKeyspaceNotificationsInitializer enableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory) {
|
||||
return new EnableRedisKeyspaceNotificationsInitializer(connectionFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that Redis is configured to send keyspace notifications. This is important to ensure that expiration and
|
||||
* deletion of sessions trigger SessionDestroyedEvents. Without the SessionDestroyedEvent resources may not get
|
||||
* cleaned up properly. For example, the mapping of the Session to WebSocket connections may not get cleaned up.
|
||||
*/
|
||||
static class EnableRedisKeyspaceNotificationsInitializer implements InitializingBean {
|
||||
static final String CONFIG_NOTIFY_KEYSPACE_EVENTS = "notify-keyspace-events";
|
||||
|
||||
private final RedisConnectionFactory connectionFactory;
|
||||
|
||||
EnableRedisKeyspaceNotificationsInitializer(RedisConnectionFactory connectionFactory) {
|
||||
this.connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
RedisConnection connection = connectionFactory.getConnection();
|
||||
String notifyOptions = getNotifyOptions(connection);
|
||||
String customizedNotifyOptions = notifyOptions;
|
||||
if(!customizedNotifyOptions.contains("E")) {
|
||||
customizedNotifyOptions += "E";
|
||||
}
|
||||
boolean A = customizedNotifyOptions.contains("A");
|
||||
if(!(A || customizedNotifyOptions.contains("g"))) {
|
||||
customizedNotifyOptions += "g";
|
||||
}
|
||||
if(!(A || customizedNotifyOptions.contains("x"))) {
|
||||
customizedNotifyOptions += "x";
|
||||
}
|
||||
if(!notifyOptions.equals(customizedNotifyOptions)) {
|
||||
connection.setConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS, customizedNotifyOptions);
|
||||
}
|
||||
}
|
||||
|
||||
private String getNotifyOptions(RedisConnection connection) {
|
||||
List<String> config = connection.getConfig(CONFIG_NOTIFY_KEYSPACE_EVENTS);
|
||||
if(config.size() < 2) {
|
||||
return "";
|
||||
}
|
||||
return config.get(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader(java.lang.ClassLoader)
|
||||
|
||||
@@ -73,8 +73,8 @@ public final class WebSocketRegistryListener implements ApplicationListener<Appl
|
||||
return;
|
||||
}
|
||||
|
||||
String id = getHttpSessionId(wsSession);
|
||||
registerWsSession(id, wsSession);
|
||||
String httpSessionId = getHttpSessionId(wsSession);
|
||||
registerWsSession(httpSessionId, wsSession);
|
||||
}
|
||||
|
||||
private String getHttpSessionId(WebSocketSession wsSession) {
|
||||
@@ -93,27 +93,33 @@ public final class WebSocketRegistryListener implements ApplicationListener<Appl
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Removal of " + wsSessionId + " was " + result);
|
||||
}
|
||||
if(sessions.isEmpty()) {
|
||||
httpSessionIdToWsSessions.remove(httpSessionId);
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Removed the corresponding HTTP Session for " + wsSessionId + " since it contained no WebSocket mappings");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerWsSession(String sessionId, WebSocketSession wsSession) {
|
||||
Map<String,WebSocketSession> sessions = httpSessionIdToWsSessions.get(sessionId);
|
||||
private void registerWsSession(String httpSessionId, WebSocketSession wsSession) {
|
||||
Map<String,WebSocketSession> sessions = httpSessionIdToWsSessions.get(httpSessionId);
|
||||
if(sessions == null) {
|
||||
sessions =
|
||||
new ConcurrentHashMap<String,WebSocketSession>();
|
||||
httpSessionIdToWsSessions.putIfAbsent(sessionId, sessions);
|
||||
sessions = httpSessionIdToWsSessions.get(sessionId);
|
||||
httpSessionIdToWsSessions.putIfAbsent(httpSessionId, sessions);
|
||||
sessions = httpSessionIdToWsSessions.get(httpSessionId);
|
||||
}
|
||||
sessions.put(wsSession.getId(), wsSession);
|
||||
}
|
||||
|
||||
private void closeWsSessions(String sessionId) {
|
||||
Map<String,WebSocketSession> sessionsToClose = httpSessionIdToWsSessions.remove(sessionId);
|
||||
private void closeWsSessions(String httpSessionId) {
|
||||
Map<String,WebSocketSession> sessionsToClose = httpSessionIdToWsSessions.remove(httpSessionId);
|
||||
if(sessionsToClose == null) {
|
||||
return;
|
||||
}
|
||||
if(logger.isDebugEnabled()) {
|
||||
logger.debug("Closing WebSocket connections associated to expired HTTP Session " + sessionId);
|
||||
logger.debug("Closing WebSocket connections associated to expired HTTP Session " + httpSessionId);
|
||||
}
|
||||
for(WebSocketSession toClose : sessionsToClose.values()) {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user