Support querying for sessions by user identifier

Fixes gh-7
This commit is contained in:
Rob Winch
2015-08-14 16:27:56 -05:00
parent 77eb6cfd71
commit 881ca7c2d4
34 changed files with 8660 additions and 14 deletions

View File

@@ -17,6 +17,9 @@ package org.springframework.session.data.redis;
import static org.fest.assertions.Assertions.assertThat;
import java.util.Map;
import java.util.UUID;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -25,11 +28,13 @@ 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.data.redis.core.RedisOperations;
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.FindByPrincipalNameSessionRepository;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
@@ -45,27 +50,36 @@ import org.springframework.test.context.web.WebAppConfiguration;
@WebAppConfiguration
public class RedisOperationsSessionRepositoryITests<S extends Session> {
@Autowired
private SessionRepository<S> repository;
private FindByPrincipalNameSessionRepository<S> repository;
@Autowired
private SessionEventRegistry registry;
@Autowired
RedisOperations<Object, Object> redis;
@Test
public void saves() throws InterruptedException {
String username = "saves-"+System.currentTimeMillis();
String usernameSessionKey = RedisOperationsSessionRepository.PRINCIPAL_NAME_PREFIX + username;
S toSave = repository.createSession();
String expectedAttributeName = "a";
String expectedAttributeValue = "b";
toSave.setAttribute(expectedAttributeName, expectedAttributeValue);
Authentication toSaveToken = new UsernamePasswordAuthenticationToken("user","password", AuthorityUtils.createAuthorityList("ROLE_USER"));
Authentication toSaveToken = new UsernamePasswordAuthenticationToken(username,"password", AuthorityUtils.createAuthorityList("ROLE_USER"));
SecurityContext toSaveContext = SecurityContextHolder.createEmptyContext();
toSaveContext.setAuthentication(toSaveToken);
toSave.setAttribute("SPRING_SECURITY_CONTEXT", toSaveContext);
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, username);
registry.clear();
repository.save(toSave);
assertThat(registry.receivedEvent()).isTrue();
assertThat(registry.getEvent()).isInstanceOf(SessionCreatedEvent.class);
assertThat(redis.boundSetOps(usernameSessionKey).members()).contains(toSave.getId());
Session session = repository.getSession(toSave.getId());
@@ -79,6 +93,7 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
assertThat(repository.getSession(toSave.getId())).isNull();
assertThat(registry.getEvent()).isInstanceOf(SessionDestroyedEvent.class);
assertThat(redis.boundSetOps(usernameSessionKey).members()).excludes(toSave.getId());
assertThat(registry.getEvent().getSession().getAttribute(expectedAttributeName)).isEqualTo(expectedAttributeValue);
@@ -103,6 +118,28 @@ public class RedisOperationsSessionRepositoryITests<S extends Session> {
assertThat(session.getAttribute("1")).isEqualTo("2");
}
@Test
public void findByPrincipalName() throws Exception {
String principalName = "findByPrincipalName" + UUID.randomUUID();
S toSave = repository.createSession();
toSave.setAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME, principalName);
repository.save(toSave);
Map<String, S> findByPrincipalName = repository.findByPrincipalName(principalName);
assertThat(findByPrincipalName).hasSize(1);
assertThat(findByPrincipalName.keySet()).containsOnly(toSave.getId());
repository.delete(toSave.getId());
registry.receivedEvent();
findByPrincipalName = repository.findByPrincipalName(principalName);
assertThat(findByPrincipalName).hasSize(0);
assertThat(findByPrincipalName.keySet()).excludes(toSave.getId());
}
static class SessionEventRegistry implements ApplicationListener<AbstractSessionEvent> {
private AbstractSessionEvent event;
private final Object lock = new Object();

View File

@@ -0,0 +1,48 @@
/*
* Copyright 2002-2015 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;
import java.util.Map;
/**
* Extends a basic {@link SessionRepository} to allow finding a session id by
* the principal name. The principal name is defined by the {@link Session}
* attribute with the name {@link Session#PRINCIPAL_NAME_ATTRIBUTE_NAME}.
*
* @author Rob Winch
*
* @param <S>
* the type of Session being managed by this
* {@link FindByPrincipalNameSessionRepository}
*/
public interface FindByPrincipalNameSessionRepository<S extends Session> extends SessionRepository<S> {
/**
* Find a Map of the session id to the {@link Session} of all sessions that
* contain the session attribute with the name
* {@link Session#PRINCIPAL_NAME_ATTRIBUTE_NAME} and the value of the
* specified principal name.
*
* @param principalName
* the principal name (i.e. username) to search for
* @return a Map (never null) of the session id to the {@link Session} of
* all sessions that contain the session attribute with the name
* {@link Session#PRINCIPAL_NAME_ATTRIBUTE_NAME} and the value of
* the specified principal name. If no results are found, an empty
* Map is returned.
*/
Map<String, S> findByPrincipalName(String principalName);
}

View File

@@ -26,6 +26,22 @@ import java.util.Set;
*/
public interface Session {
/**
* <p>
* A common session attribute that contains the current principal name (i.e.
* username).
* </p>
*
* <p>
* It is the responsibility of the developer to ensure the attribute
* is populated since Spring Session is not aware of the authentication
* mechanism being used.
* </p>
*
* @since 1.1
*/
String PRINCIPAL_NAME_ATTRIBUTE_NAME = Session.class.getName().concat(".PRINCIPAL_NAME_ATTRIBUTE_NAME");
/**
* Gets a unique string that identifies the {@link Session}
*

View File

@@ -35,6 +35,7 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.session.ExpiringSession;
import org.springframework.session.FindByPrincipalNameSessionRepository;
import org.springframework.session.MapSession;
import org.springframework.session.Session;
import org.springframework.session.SessionRepository;
@@ -246,23 +247,28 @@ import org.springframework.util.Assert;
*
* @author Rob Winch
*/
public class RedisOperationsSessionRepository implements SessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
public class RedisOperationsSessionRepository implements FindByPrincipalNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener {
private static final Log logger = LogFactory.getLog(SessionMessageListener.class);
/**
* The prefix for each key 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:event:created:";
private static final Log logger = LogFactory.getLog(SessionMessageListener.class);
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
public void publishEvent(ApplicationEvent event) {
}
};
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:sessions:";
static final String BOUNDED_HASH_KEY_PREFIX = SPRING_SESSION_KEY_PREFIX + "sessions:";
/**
* The key in the Hash representing {@link org.springframework.session.ExpiringSession#getCreationTime()}
@@ -290,6 +296,11 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
private final RedisSessionExpirationPolicy expirationPolicy;
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() {
public void publishEvent(ApplicationEvent event) {
}
};
/**
* If non-null, this value is used to override the default value for {@link RedisSession#setMaxInactiveIntervalInSeconds(int)}.
*/
@@ -357,6 +368,24 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
return getSession(id, false);
}
public Map<String,RedisSession> findByPrincipalName(String principalName) {
String principalKey = getPrincipalKey(principalName);
Set<Object> sessionIds = sessionRedisOperations.boundSetOps(principalKey).members();
Map<String,RedisSession> sessions = new HashMap<String,RedisSession>(sessionIds.size());
for(Object id : sessionIds) {
RedisSession session = getSession((String) id);
if(session != null) {
session.setLastAccessedTime(session.originalLastAccessTime);
sessions.put(session.getId(), session);
}
}
return sessions;
}
private String getPrincipalKey(String principalName) {
return PRINCIPAL_NAME_PREFIX + principalName;
}
/**
*
* @param id the session id
@@ -376,7 +405,7 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
return null;
}
RedisSession result = new RedisSession(loaded);
result.originalLastAccessTime = loaded.getLastAccessedTime() + TimeUnit.SECONDS.toMillis(loaded.getMaxInactiveIntervalInSeconds());
result.originalLastAccessTime = loaded.getLastAccessedTime();
result.setLastAccessedTime(System.currentTimeMillis());
return result;
}
@@ -459,6 +488,11 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
logger.debug("Publishing SessionDestroyedEvent for session " + sessionId);
}
String principal = (String) session.getAttribute(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
if(principal != null) {
sessionRedisOperations.boundSetOps(getPrincipalKey(principal)).remove(sessionId);
}
if(isDeleted) {
handleDeleted(sessionId, session);
} else {
@@ -640,9 +674,17 @@ public class RedisOperationsSessionRepository implements SessionRepository<Redis
private void saveDelta() {
String sessionId = getId();
getSessionBoundHashOperations(sessionId).putAll(delta);
String key = getSessionAttrNameKey(Session.PRINCIPAL_NAME_ATTRIBUTE_NAME);
if(delta.containsKey(key)) {
Object principal = delta.get(key);
String principalKey = getPrincipalKey((String) principal);
sessionRedisOperations.boundSetOps(principalKey).add(sessionId);
}
delta = new HashMap<String,Object>(delta.size());
expirationPolicy.onExpirationUpdated(originalLastAccessTime, this);
Long originalExpiration = originalLastAccessTime == null ? null : originalLastAccessTime + TimeUnit.SECONDS.toMillis(getMaxInactiveIntervalInSeconds()) ;
expirationPolicy.onExpirationUpdated(originalExpiration, this);
}
}
}

View File

@@ -27,6 +27,7 @@ import static org.springframework.session.data.redis.RedisOperationsSessionRepos
import static org.springframework.session.data.redis.RedisOperationsSessionRepository.getSessionAttrNameKey;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -308,6 +309,46 @@ public class RedisOperationsSessionRepositoryTests {
assertThat(redisRepository.getSession(expiredId)).isNull();
}
@Test
public void findByPrincipalNameExpired() {
String expiredId = "expired-id";
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
when(boundSetOperations.members()).thenReturn(Collections.<Object>singleton(expiredId));
when(redisOperations.boundHashOps(getKey(expiredId))).thenReturn(boundHashOperations);
Map map = map(
MAX_INACTIVE_ATTR, 1,
LAST_ACCESSED_ATTR, System.currentTimeMillis() - TimeUnit.MINUTES.toMillis(5));
when(boundHashOperations.entries()).thenReturn(map);
assertThat(redisRepository.findByPrincipalName("principal")).isEmpty();
}
@Test
public void findByPrincipalName() {
long lastAccessed = System.currentTimeMillis() - 10;
long createdTime = lastAccessed - 10;
int maxInactive = 3600;
String sessionId = "some-id";
when(redisOperations.boundSetOps(anyString())).thenReturn(boundSetOperations);
when(boundSetOperations.members()).thenReturn(Collections.<Object>singleton(sessionId));
when(redisOperations.boundHashOps(getKey(sessionId))).thenReturn(boundHashOperations);
Map map = map(
CREATION_TIME_ATTR, createdTime,
MAX_INACTIVE_ATTR, maxInactive,
LAST_ACCESSED_ATTR, lastAccessed);
when(boundHashOperations.entries()).thenReturn(map);
Map<String, RedisSession> sessionIdToSessions = redisRepository.findByPrincipalName("principal");
assertThat(sessionIdToSessions).hasSize(1);
RedisSession session = sessionIdToSessions.get(sessionId);
assertThat(session).isNotNull();
assertThat(session.getId()).isEqualTo(sessionId);
assertThat(session.getLastAccessedTime()).isEqualTo(lastAccessed);
assertThat(session.getMaxInactiveIntervalInSeconds()).isEqualTo(maxInactive);
assertThat(session.getCreationTime()).isEqualTo(createdTime);
}
@Test
public void cleanupExpiredSessions() {
String expiredId = "expired-id";