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:
@@ -5,14 +5,21 @@ import static org.fest.assertions.Assertions.assertThat;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
@@ -21,9 +28,11 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
import org.springframework.session.events.SessionDestroyedEvent;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import redis.clients.jedis.Protocol;
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@@ -34,19 +43,18 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
@Autowired
|
||||
private SessionRepository<S> repository;
|
||||
|
||||
@Before
|
||||
public void setup() throws IOException {
|
||||
redisServer = new RedisServer(getPort());
|
||||
redisServer.start();
|
||||
}
|
||||
@Autowired
|
||||
private SessionDestroyedEventRegistry registry;
|
||||
|
||||
@After
|
||||
public void shutdown() throws InterruptedException {
|
||||
redisServer.stop();
|
||||
private final Object lock = new Object();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
registry.setLock(lock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saves() {
|
||||
public void saves() throws InterruptedException {
|
||||
S toSave = repository.createSession();
|
||||
toSave.setAttribute("a", "b");
|
||||
Authentication toSaveToken = new UsernamePasswordAuthenticationToken("user","password", AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
@@ -65,6 +73,10 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
repository.delete(toSave.getId());
|
||||
|
||||
assertThat(repository.getSession(toSave.getId())).isNull();
|
||||
synchronized (lock) {
|
||||
lock.wait(3000);
|
||||
}
|
||||
assertThat(registry.receivedEvent()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -86,6 +98,27 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
assertThat(session.getAttribute("1")).isEqualTo("2");
|
||||
}
|
||||
|
||||
static class SessionDestroyedEventRegistry implements ApplicationListener<SessionDestroyedEvent> {
|
||||
private boolean receivedEvent;
|
||||
private Object lock;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(SessionDestroyedEvent event) {
|
||||
receivedEvent = true;
|
||||
synchronized (lock) {
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean receivedEvent() {
|
||||
return receivedEvent;
|
||||
}
|
||||
|
||||
public void setLock(Object lock) {
|
||||
this.lock = lock;
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableRedisHttpSession
|
||||
static class Config {
|
||||
@@ -96,6 +129,46 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
factory.setUsePool(false);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public static RedisServerBean redisServer() {
|
||||
return new RedisServerBean();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionDestroyedEventRegistry sessionDestroyedEventRegistry() {
|
||||
return new SessionDestroyedEventRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements BeanDefinitionRegistryPostProcessor to ensure this Bean
|
||||
* is initialized before any other Beans. Specifically, we want to ensure
|
||||
* that the Redis Server is started before RedisHttpSessionConfiguration
|
||||
* attempts to enable Keyspace notifications.
|
||||
*/
|
||||
static class RedisServerBean implements InitializingBean, DisposableBean, BeanDefinitionRegistryPostProcessor {
|
||||
private RedisServer redisServer;
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
redisServer = new RedisServer(getPort());
|
||||
redisServer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if(redisServer != null) {
|
||||
redisServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
|
||||
}
|
||||
}
|
||||
|
||||
private static Integer availablePort;
|
||||
@@ -106,6 +179,6 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
|
||||
availablePort = socket.getLocalPort();
|
||||
socket.close();
|
||||
}
|
||||
return availablePort;
|
||||
return Protocol.DEFAULT_PORT;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2002-2014 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
|
||||
* use this file except in compliance with the License. You may obtain a copy of
|
||||
* the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
* License for the specific language governing permissions and limitations under
|
||||
* the License.
|
||||
*/
|
||||
package org.springframework.session.data.redis.config.annotation.web.http;
|
||||
|
||||
|
||||
import static org.fest.assertions.Assertions.assertThat;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.DisposableBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
|
||||
import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.AuthorityUtils;
|
||||
import org.springframework.security.core.context.SecurityContext;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.session.Session;
|
||||
import org.springframework.session.SessionRepository;
|
||||
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
|
||||
import org.springframework.session.events.SessionDestroyedEvent;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import redis.clients.jedis.Protocol;
|
||||
import redis.embedded.RedisServer;
|
||||
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration
|
||||
public class EnableRedisHttpSessionExpireSessionDestroyedTests<S extends Session> {
|
||||
private RedisServer redisServer;
|
||||
|
||||
@Autowired
|
||||
private SessionRepository<S> repository;
|
||||
|
||||
@Autowired
|
||||
private SessionDestroyedEventRegistry registry;
|
||||
|
||||
private final Object lock = new Object();
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
registry.setLock(lock);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void expireFiresSessionDestroyedEvent() throws InterruptedException {
|
||||
S toSave = repository.createSession();
|
||||
toSave.setAttribute("a", "b");
|
||||
Authentication toSaveToken = new UsernamePasswordAuthenticationToken("user","password", AuthorityUtils.createAuthorityList("ROLE_USER"));
|
||||
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
|
||||
toSaveContext.setAuthentication(toSaveToken);
|
||||
toSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
|
||||
|
||||
repository.save(toSave);
|
||||
|
||||
synchronized (lock) {
|
||||
lock.wait(1100);
|
||||
}
|
||||
if(!registry.receivedEvent()) {
|
||||
// Redis makes no guarantees on when an expired event will be fired
|
||||
// we can ensure it gets fired by trying to get the session
|
||||
repository.getSession(toSave.getId());
|
||||
}
|
||||
assertThat(registry.receivedEvent()).isTrue();
|
||||
}
|
||||
|
||||
static class SessionDestroyedEventRegistry implements ApplicationListener<SessionDestroyedEvent> {
|
||||
private boolean receivedEvent;
|
||||
private Object lock;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(SessionDestroyedEvent event) {
|
||||
receivedEvent = true;
|
||||
synchronized (lock) {
|
||||
lock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean receivedEvent() {
|
||||
return receivedEvent;
|
||||
}
|
||||
|
||||
public void setLock(Object lock) {
|
||||
this.lock = lock;
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1)
|
||||
static class Config {
|
||||
@Bean
|
||||
public JedisConnectionFactory connectionFactory() throws Exception {
|
||||
JedisConnectionFactory factory = new JedisConnectionFactory();
|
||||
factory.setPort(getPort());
|
||||
factory.setUsePool(false);
|
||||
return factory;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public static RedisServerBean redisServer() {
|
||||
return new RedisServerBean();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SessionDestroyedEventRegistry sessionDestroyedEventRegistry() {
|
||||
return new SessionDestroyedEventRegistry();
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements BeanDefinitionRegistryPostProcessor to ensure this Bean
|
||||
* is initialized before any other Beans. Specifically, we want to ensure
|
||||
* that the Redis Server is started before RedisHttpSessionConfiguration
|
||||
* attempts to enable Keyspace notifications.
|
||||
*/
|
||||
static class RedisServerBean implements InitializingBean, DisposableBean, BeanDefinitionRegistryPostProcessor {
|
||||
private RedisServer redisServer;
|
||||
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() throws Exception {
|
||||
redisServer = new RedisServer(getPort());
|
||||
redisServer.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroy() throws Exception {
|
||||
if(redisServer != null) {
|
||||
redisServer.stop();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {}
|
||||
|
||||
@Override
|
||||
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {}
|
||||
}
|
||||
}
|
||||
|
||||
private static Integer availablePort;
|
||||
|
||||
private static int getPort() throws IOException {
|
||||
if(availablePort == null) {
|
||||
ServerSocket socket = new ServerSocket(0);
|
||||
availablePort = socket.getLocalPort();
|
||||
socket.close();
|
||||
}
|
||||
return Protocol.DEFAULT_PORT;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user