Support querying for sessions by user identifier
Fixes gh-7
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user