Support application specific prefix

Fixes gh-166
This commit is contained in:
Rob Winch
2015-08-17 10:02:19 -05:00
parent 6234dd5681
commit db45698e25
9 changed files with 120 additions and 51 deletions

View File

@@ -340,6 +340,16 @@ include::{indexdoc-tests}[tags=new-redisoperationssessionrepository]
For additional information on how to create a `RedisConnectionFactory`, refer to the Spring Data Redis Reference.
[[api-redisoperationssessionrepository-config]]
==== EnableRedisHttpSession
In a web environment, the simplest way to create a new `RedisOperationsSessionRepository` is to use `@EnableRedisHttpSession`.
Complete example usage can be found in the <<samples>>
You can use the following attributes to customize the configuration:
* **maxInactiveIntervalInSeconds** - the amount of time before the session will expire in seconds
* **redisNamespace** - allows configuring an application specific namespace for the sessions. Redis keys and channel ids will start with the prefix of `spring:session:<redisNamespace>:`.
[[api-redisoperationssessionrepository-storage]]
==== Storage Details

View File

@@ -31,6 +31,8 @@ task integrationTomcatRun(type: org.gradle.api.plugins.tomcat.tasks.TomcatRun) {
httpPort = ports[0]
ajpPort = ports[1]
stopPort = ports[2]
System.setProperty('spring.session.redis.namespace',project.name)
}
}

View File

@@ -40,6 +40,8 @@ integrationTest {
systemProperties['geb.build.reportsDir'] = 'build/geb-reports'
systemProperties['server.port'] = port
systemProperties['management.port'] = 0
systemProperties['spring.session.redis.namespace'] = project.name
}
}

View File

@@ -43,6 +43,8 @@ integrationTest {
systemProperties['geb.build.reportsDir'] = 'build/geb-reports'
systemProperties['server.port'] = port
systemProperties['management.port'] = 0
systemProperties['spring.session.redis.namespace'] = project.name
}
}

View File

@@ -251,24 +251,9 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
private static final Log logger = LogFactory.getLog(SessionMessageListener.class);
/**
* The prefix for each key in Redis used by Spring Session
* The default prefix for each key and channel in Redis used by Spring Session
*/
static final String SPRING_SESSION_KEY_PREFIX = "spring:session:";
/**
* The prefix for each key that contains a mapping of the Principal name (i.e. username) to the session ids.
*/
static final String PRINCIPAL_NAME_PREFIX = SPRING_SESSION_KEY_PREFIX + "index:" + Session.PRINCIPAL_NAME_ATTRIBUTE_NAME + ":";
/**
* The prefix for SessionCreated event channel. The suffix is the session id.
*/
private static final String SPRING_SESSION_CREATED_PREFIX = SPRING_SESSION_KEY_PREFIX + "event:created:";
/**
* The prefix for each key of the Redis Hash representing a single session. The suffix is the unique session id.
*/
static final String BOUNDED_HASH_KEY_PREFIX = SPRING_SESSION_KEY_PREFIX + "sessions:";
static final String DEFAULT_SPRING_SESSION_REDIS_PREFIX = "spring:session:";
/**
* The key in the Hash representing {@link org.springframework.session.ExpiringSession#getCreationTime()}
@@ -292,6 +277,11 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
*/
static final String SESSION_ATTR_PREFIX = "sessionAttr:";
/**
* The prefix for every key used by Spring Session in Redis.
*/
private String keyPrefix = DEFAULT_SPRING_SESSION_REDIS_PREFIX;
private final RedisOperations<Object,Object> sessionRedisOperations;
private final RedisSessionExpirationPolicy expirationPolicy;
@@ -323,7 +313,7 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
public RedisOperationsSessionRepository(RedisOperations<Object, Object> sessionRedisOperations) {
Assert.notNull(sessionRedisOperations, "sessionRedisOperations cannot be null");
this.sessionRedisOperations = sessionRedisOperations;
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations);
this.expirationPolicy = new RedisSessionExpirationPolicy(sessionRedisOperations, this);
}
/**
@@ -354,7 +344,8 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
public void save(RedisSession session) {
session.saveDelta();
if(session.isNew()) {
this.sessionRedisOperations.convertAndSend(SPRING_SESSION_CREATED_PREFIX + session.getId(), session.delta);
String sessionCreatedKey = getSessionCreatedChannel(session.getId());
this.sessionRedisOperations.convertAndSend(sessionCreatedKey, session.delta);
session.setNew(false);
}
}
@@ -382,10 +373,6 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
return sessions;
}
private String getPrincipalKey(String principalName) {
return PRINCIPAL_NAME_PREFIX + principalName;
}
/**
*
* @param id the session id
@@ -443,10 +430,6 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
save(session);
}
private String getExpiredKey(String sessionId) {
return getKey("expires:" + sessionId);
}
public RedisSession createSession() {
RedisSession redisSession = new RedisSession();
if(defaultMaxInactiveInterval != null) {
@@ -464,7 +447,7 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
String channel = new String(messageChannel);
if(channel.startsWith(SPRING_SESSION_CREATED_PREFIX)) {
if(channel.startsWith(getSessionCreatedChannelPrefix())) {
RedisSerializer<Object> serializer = new JdkSerializationRedisSerializer();
Map<Object,Object> loaded = (Map<Object, Object>) serializer.deserialize(message.getBody());
handleCreated(loaded, channel);
@@ -472,7 +455,7 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
}
String body = new String(messageBody);
if(!body.startsWith("spring:session:sessions:expires")) {
if(!body.startsWith(getExpiredKeyPrefix())) {
return;
}
@@ -534,14 +517,57 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
}
}
public void setRedisKeyNamespace(String namespace) {
this.keyPrefix = DEFAULT_SPRING_SESSION_REDIS_PREFIX + namespace + ":";
}
/**
* Gets the Hash key for this session by prefixing it appropriately.
*
* @param sessionId the session id
* @return the Hash key for this session by prefixing it appropriately.
*/
static String getKey(String sessionId) {
return BOUNDED_HASH_KEY_PREFIX + sessionId;
String getSessionKey(String sessionId) {
return this.keyPrefix + "sessions:" + sessionId;
}
String getPrincipalKey(String principalName) {
return this.keyPrefix + "index:" + Session.PRINCIPAL_NAME_ATTRIBUTE_NAME + ":" + principalName;
}
String getExpirationsKey(long expiration) {
return this.keyPrefix + "expirations:" + expiration;
}
private String getExpiredKey(String sessionId) {
return getExpiredKeyPrefix() + sessionId;
}
private String getSessionCreatedChannel(String sessionId) {
return getSessionCreatedChannelPrefix() + sessionId;
}
private String getExpiredKeyPrefix() {
return this.keyPrefix + "sessions:" + "expires:";
}
/**
* Gets the prefix for the channel that SessionCreatedEvent are published to. The suffix is the session id of the session that was created.
*
* @return
*/
public String getSessionCreatedChannelPrefix() {
return this.keyPrefix + "event:created:";
}
/**
* Gets the {@link BoundHashOperations} to operate on a {@link Session}
* @param sessionId the id of the {@link Session} to work with
* @return the {@link BoundHashOperations} to operate on a {@link Session}
*/
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
String key = getSessionKey(sessionId);
return this.sessionRedisOperations.boundHashOps(key);
}
/**
@@ -554,16 +580,6 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess
return SESSION_ATTR_PREFIX + attributeName;
}
/**
* Gets the {@link BoundHashOperations} to operate on a {@link Session}
* @param sessionId the id of the {@link Session} to work with
* @return the {@link BoundHashOperations} to operate on a {@link Session}
*/
private BoundHashOperations<Object, Object, Object> getSessionBoundHashOperations(String sessionId) {
String key = getKey(sessionId);
return this.sessionRedisOperations.boundHashOps(key);
}
private static RedisTemplate<Object,Object> createDefaultTemplate(RedisConnectionFactory connectionFactory) {
Assert.notNull(connectionFactory,"connectionFactory cannot be null");
RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();

View File

@@ -48,17 +48,16 @@ final class RedisSessionExpirationPolicy {
private static final Log logger = LogFactory.getLog(RedisOperationsSessionRepository.class);
/**
* The prefix for each key of the Redis Hash representing a single session. The suffix is the unique session id.
*/
static final String EXPIRATION_BOUNDED_HASH_KEY_PREFIX = "spring:session:expirations:";
private final RedisOperations<Object,Object> redis;
private final RedisOperationsSessionRepository redisSession;
public RedisSessionExpirationPolicy(
RedisOperations<Object,Object> sessionRedisOperations) {
RedisOperations<Object,Object> sessionRedisOperations, RedisOperationsSessionRepository redisSession) {
super();
this.redis = sessionRedisOperations;
this.redisSession = redisSession;
}
public void onDelete(ExpiringSession session) {
@@ -92,11 +91,11 @@ final class RedisSessionExpirationPolicy {
}
String getExpirationKey(long expires) {
return EXPIRATION_BOUNDED_HASH_KEY_PREFIX + expires;
return this.redisSession.getExpirationsKey(expires);
}
String getSessionKey(String sessionId) {
return RedisOperationsSessionRepository.BOUNDED_HASH_KEY_PREFIX + sessionId;
return this.redisSession.getSessionKey(sessionId);
}
public void cleanExpiredSessions() {

View File

@@ -28,7 +28,6 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
* SessionRepositoryFilter as a bean named "springSessionRepositoryFilter" and
* backed by Redis. In order to leverage the annotation, a single {@link RedisConnectionFactory}
* must be provided. For example:
*
* <pre>
* {@literal @Configuration}
* {@literal @EnableRedisHttpSession}
@@ -54,4 +53,23 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
@Configuration
public @interface EnableRedisHttpSession {
int maxInactiveIntervalInSeconds() default 1800;
/**
* <p>
* Defines a unique namespace for keys. The value is used to isolate
* sessions by changing the prefix from "spring:session:" to
* "spring:session:&lt;redisNamespace&gt;:". The default is "" such that all Redis
* keys begin with "spring:session".
* </p>
*
* <p>
* For example, if you had an application named "Application A" that needed
* to keep the sessions isolated from "Application B" you could set two
* different values for the applications and they could function within the
* same Redis instance.
* </p>
*
* @return
*/
String redisNamespace() default "";
}

View File

@@ -51,6 +51,7 @@ import org.springframework.session.web.http.HttpSessionStrategy;
import org.springframework.session.web.http.SessionEventHttpSessionListenerAdapter;
import org.springframework.session.web.http.SessionRepositoryFilter;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
/**
* Exposes the {@link SessionRepositoryFilter} as a bean named
@@ -76,6 +77,8 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
private List<HttpSessionListener> httpSessionListeners = new ArrayList<HttpSessionListener>();
private String redisNamespace = "";
@Bean
public SessionEventHttpSessionListenerAdapter sessionEventHttpSessionListenerAdapter() {
return new SessionEventHttpSessionListenerAdapter(httpSessionListeners);
@@ -89,7 +92,7 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
container.setConnectionFactory(connectionFactory);
container.addMessageListener(messageListener,
Arrays.asList(new PatternTopic("__keyevent@*:del"), new PatternTopic("__keyevent@*:expired")));
container.addMessageListener(messageListener, Arrays.asList(new PatternTopic("spring:session:event:created:*")));
container.addMessageListener(messageListener, Arrays.asList(new PatternTopic(messageListener.getSessionCreatedChannelPrefix() + "*")));
return container;
}
@@ -107,6 +110,11 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository(sessionRedisTemplate);
sessionRepository.setApplicationEventPublisher(applicationEventPublisher);
sessionRepository.setDefaultMaxInactiveInterval(maxInactiveIntervalInSeconds);
String redisNamespace = getRedisNamespace();
if(StringUtils.hasText(redisNamespace)) {
sessionRepository.setRedisKeyNamespace(redisNamespace);
}
return sessionRepository;
}
@@ -124,6 +132,17 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
this.maxInactiveIntervalInSeconds = maxInactiveIntervalInSeconds;
}
public void setRedisNamespace(String namespace) {
this.redisNamespace = namespace;
}
private String getRedisNamespace() {
if(StringUtils.hasText(this.redisNamespace)) {
return this.redisNamespace;
}
return System.getProperty("spring.session.redis.namespace","");
}
public void setImportMetadata(AnnotationMetadata importMetadata) {
Map<String, Object> enableAttrMap = importMetadata.getAnnotationAttributes(EnableRedisHttpSession.class.getName());
@@ -142,6 +161,7 @@ public class RedisHttpSessionConfiguration implements ImportAware, BeanClassLoad
}
}
maxInactiveIntervalInSeconds = enableAttrs.getNumber("maxInactiveIntervalInSeconds");
this.redisNamespace = enableAttrs.getString("redisNamespace");
}
@Autowired(required = false)

View File

@@ -57,7 +57,7 @@ public class RedisSessionExpirationPolicyTests {
@Before
public void setup() {
RedisOperationsSessionRepository repository = new RedisOperationsSessionRepository(sessionRedisOperations);
policy = new RedisSessionExpirationPolicy(sessionRedisOperations);
policy = new RedisSessionExpirationPolicy(sessionRedisOperations, repository);
session = new MapSession();
session.setLastAccessedTime(1429116694665L);
session.setId("12345");