Support application specific prefix
Fixes gh-166
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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:<redisNamespace>:". 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 "";
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user