diff --git a/docs/src/docs/asciidoc/index.adoc b/docs/src/docs/asciidoc/index.adoc index 761d3f4..70c1fcf 100644 --- a/docs/src/docs/asciidoc/index.adoc +++ b/docs/src/docs/asciidoc/index.adoc @@ -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 <> +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::`. + [[api-redisoperationssessionrepository-storage]] ==== Storage Details diff --git a/gradle/tomcat.gradle b/gradle/tomcat.gradle index 5d83bd3..087778c 100644 --- a/gradle/tomcat.gradle +++ b/gradle/tomcat.gradle @@ -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) } } diff --git a/samples/boot/build.gradle b/samples/boot/build.gradle index 592597e..bdd0c20 100644 --- a/samples/boot/build.gradle +++ b/samples/boot/build.gradle @@ -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 } } diff --git a/samples/findbyusername/build.gradle b/samples/findbyusername/build.gradle index 398bef5..91eb7f6 100644 --- a/samples/findbyusername/build.gradle +++ b/samples/findbyusername/build.gradle @@ -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 } } diff --git a/spring-session/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java b/spring-session/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java index dc0287e..c36d1eb 100644 --- a/spring-session/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java +++ b/spring-session/src/main/java/org/springframework/session/data/redis/RedisOperationsSessionRepository.java @@ -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 sessionRedisOperations; private final RedisSessionExpirationPolicy expirationPolicy; @@ -323,7 +313,7 @@ public class RedisOperationsSessionRepository implements FindByPrincipalNameSess public RedisOperationsSessionRepository(RedisOperations 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 serializer = new JdkSerializationRedisSerializer(); Map loaded = (Map) 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 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 getSessionBoundHashOperations(String sessionId) { - String key = getKey(sessionId); - return this.sessionRedisOperations.boundHashOps(key); - } - private static RedisTemplate createDefaultTemplate(RedisConnectionFactory connectionFactory) { Assert.notNull(connectionFactory,"connectionFactory cannot be null"); RedisTemplate template = new RedisTemplate(); diff --git a/spring-session/src/main/java/org/springframework/session/data/redis/RedisSessionExpirationPolicy.java b/spring-session/src/main/java/org/springframework/session/data/redis/RedisSessionExpirationPolicy.java index 5175a85..ab41918 100644 --- a/spring-session/src/main/java/org/springframework/session/data/redis/RedisSessionExpirationPolicy.java +++ b/spring-session/src/main/java/org/springframework/session/data/redis/RedisSessionExpirationPolicy.java @@ -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 redis; + private final RedisOperationsSessionRepository redisSession; + public RedisSessionExpirationPolicy( - RedisOperations sessionRedisOperations) { + RedisOperations 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() { diff --git a/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java b/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java index aaf1001..5803df1 100644 --- a/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java +++ b/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/EnableRedisHttpSession.java @@ -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: - * *
  * {@literal @Configuration}
  * {@literal @EnableRedisHttpSession}
@@ -54,4 +53,23 @@ import org.springframework.data.redis.connection.RedisConnectionFactory;
 @Configuration
 public @interface EnableRedisHttpSession {
 	int maxInactiveIntervalInSeconds() default 1800;
+
+	/**
+	 * 

+ * Defines a unique namespace for keys. The value is used to isolate + * sessions by changing the prefix from "spring:session:" to + * "spring:session:<redisNamespace>:". The default is "" such that all Redis + * keys begin with "spring:session". + *

+ * + *

+ * 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. + *

+ * + * @return + */ + String redisNamespace() default ""; } \ No newline at end of file diff --git a/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java b/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java index 3045b4b..11be1ea 100644 --- a/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java +++ b/spring-session/src/main/java/org/springframework/session/data/redis/config/annotation/web/http/RedisHttpSessionConfiguration.java @@ -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 httpSessionListeners = new ArrayList(); + 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 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) diff --git a/spring-session/src/test/java/org/springframework/session/data/redis/RedisSessionExpirationPolicyTests.java b/spring-session/src/test/java/org/springframework/session/data/redis/RedisSessionExpirationPolicyTests.java index d5891a6..a0c3cd9 100644 --- a/spring-session/src/test/java/org/springframework/session/data/redis/RedisSessionExpirationPolicyTests.java +++ b/spring-session/src/test/java/org/springframework/session/data/redis/RedisSessionExpirationPolicyTests.java @@ -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");