1
0
mirror of synced 2026-08-07 10:47:49 +00:00

Refactored SessionRegistryImpl to remove servlet API deps and moved back into core, along with other concurrent authentication package classes.

This commit is contained in:
Luke Taylor
2009-04-21 06:05:14 +00:00
parent 75d5e8f5f2
commit cac2bce382
12 changed files with 80 additions and 114 deletions
@@ -1,34 +0,0 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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.security.authentication.concurrent;
import org.springframework.security.core.AuthenticationException;
/**
* Thrown by a <code>SessionRegistry</code> implementation if an attempt is made to create new session information
* for an existing sessionId. The user should firstly clear the existing session from the
* <code>ConcurrentSessionRegistry</code>.
*
* @author Ben Alex
*/
public class SessionAlreadyUsedException extends AuthenticationException {
//~ Constructors ===================================================================================================
public SessionAlreadyUsedException(String msg) {
super(msg);
}
}
@@ -67,11 +67,8 @@ public interface SessionRegistry {
*
* @param sessionId to associate with the principal (should never be <code>null</code>)
* @param principal to associate with the session (should never be <code>null</code>)
*
* @throws SessionAlreadyUsedException DOCUMENT ME!
*/
void registerNewSession(String sessionId, Object principal)
throws SessionAlreadyUsedException;
void registerNewSession(String sessionId, Object principal);
/**
* Deletes all the session information being maintained for the specified <code>sessionId</code>. If the
@@ -0,0 +1,172 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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.security.authentication.concurrent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.security.core.session.SessionDestroyedEvent;
import org.springframework.util.Assert;
/**
* Base implementation of {@link org.springframework.security.authentication.concurrent.SessionRegistry}
* which also listens for {@link org.springframework.security.web.session.HttpSessionDestroyedEvent}s
* published in the Spring application context.
*
* <p>
* NB: It is important that you register the {@link org.springframework.security.web.session.HttpSessionEventPublisher} in
* <code>web.xml</code> so that this class is notified of sessions that expire.
* </p>
*
* @author Ben Alex
* @version $Id$
*/
public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> {
//~ Static fields/initializers =====================================================================================
protected static final Log logger = LogFactory.getLog(SessionRegistryImpl.class);
// ~ Instance fields ===============================================================================================
/** <principal:Object,SessionIdSet> */
private Map<Object,Set<String>> principals = Collections.synchronizedMap(new HashMap<Object,Set<String>>());
/** <sessionId:Object,SessionInformation> */
private Map<String, SessionInformation> sessionIds = Collections.synchronizedMap(new HashMap<String, SessionInformation>());
// ~ Methods =======================================================================================================
public Object[] getAllPrincipals() {
return principals.keySet().toArray();
}
public SessionInformation[] getAllSessions(Object principal, boolean includeExpiredSessions) {
Set<String> sessionsUsedByPrincipal = principals.get(principal);
if (sessionsUsedByPrincipal == null) {
return null;
}
List<SessionInformation> list = new ArrayList<SessionInformation>();
synchronized (sessionsUsedByPrincipal) {
for (String sessionId : sessionsUsedByPrincipal) {
SessionInformation sessionInformation = getSessionInformation(sessionId);
if (sessionInformation == null) {
continue;
}
if (includeExpiredSessions || !sessionInformation.isExpired()) {
list.add(sessionInformation);
}
}
}
return (SessionInformation[]) list.toArray(new SessionInformation[0]);
}
public SessionInformation getSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
return (SessionInformation) sessionIds.get(sessionId);
}
public void onApplicationEvent(SessionDestroyedEvent event) {
String sessionId = event.getId();
removeSessionInformation(sessionId);
}
public void refreshLastRequest(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = getSessionInformation(sessionId);
if (info != null) {
info.refreshLastRequest();
}
}
public synchronized void registerNewSession(String sessionId, Object principal) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
Assert.notNull(principal, "Principal required as per interface contract");
if (logger.isDebugEnabled()) {
logger.debug("Registering session " + sessionId +", for principal " + principal);
}
if (getSessionInformation(sessionId) != null) {
removeSessionInformation(sessionId);
}
sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date()));
Set<String> sessionsUsedByPrincipal = principals.get(principal);
if (sessionsUsedByPrincipal == null) {
sessionsUsedByPrincipal = Collections.synchronizedSet(new HashSet<String>(4));
principals.put(principal, sessionsUsedByPrincipal);
}
sessionsUsedByPrincipal.add(sessionId);
}
public void removeSessionInformation(String sessionId) {
Assert.hasText(sessionId, "SessionId required as per interface contract");
SessionInformation info = getSessionInformation(sessionId);
if (info == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Removing session " + sessionId + " from set of registered sessions");
}
sessionIds.remove(sessionId);
Set<String> sessionsUsedByPrincipal = principals.get(info.getPrincipal());
if (sessionsUsedByPrincipal == null) {
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Removing session " + sessionId + " from principal's set of registered sessions");
}
synchronized (sessionsUsedByPrincipal) {
sessionsUsedByPrincipal.remove(sessionId);
if (sessionsUsedByPrincipal.size() == 0) {
// No need to keep object in principals Map anymore
if (logger.isDebugEnabled()) {
logger.debug("Removing principal " + info.getPrincipal() + " from registry");
}
principals.remove(info.getPrincipal());
}
}
}
}
@@ -23,4 +23,10 @@ public abstract class SessionDestroyedEvent extends ApplicationEvent {
* @return the <tt>SecurityContext</tt> associated with the session, or null if there is no context.
*/
public abstract SecurityContext getSecurityContext();
/**
* The identifier associated with the destroyed session.
* @return
*/
public abstract String getId();
}
@@ -0,0 +1,112 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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.security.authentication.concurrent;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.concurrent.ConcurrentLoginException;
import org.springframework.security.authentication.concurrent.ConcurrentSessionControllerImpl;
import org.springframework.security.authentication.concurrent.SessionIdentifierAware;
import org.springframework.security.authentication.concurrent.SessionRegistry;
import org.springframework.security.authentication.concurrent.SessionRegistryImpl;
import org.springframework.security.core.Authentication;
/**
* Tests {@link ConcurrentSessionControllerImpl}.
*
* @author Ben Alex
* @version $Id$
*/
public class ConcurrentSessionControllerImplTests {
//~ Methods ========================================================================================================
private static int nextSessionId = 1000;
private Authentication createAuthentication(String user, String password) {
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, password);
auth.setDetails(new SessionIdentifierAware() {
private final String id = Integer.toString(nextSessionId++);
public String getSessionId() {
return id;
}
});
return auth;
}
@Test
public void testLifecycle() throws Exception {
// Build a test fixture
ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl();
SessionRegistry registry = new SessionRegistryImpl();
sc.setSessionRegistry(registry);
// Attempt to authenticate - it should be successful
Authentication auth = createAuthentication("bob", "1212");
sc.checkAuthenticationAllowed(auth);
sc.registerSuccessfulAuthentication(auth);
String sessionId1 = ((SessionIdentifierAware) auth.getDetails()).getSessionId();
assertFalse(registry.getSessionInformation(sessionId1).isExpired());
// Attempt to authenticate again - it should still be successful
sc.checkAuthenticationAllowed(auth);
sc.registerSuccessfulAuthentication(auth);
// Attempt to authenticate with a different session for same principal - should fail
sc.setExceptionIfMaximumExceeded(true);
Authentication auth2 = createAuthentication("bob", "1212");
assertFalse(registry.getSessionInformation(sessionId1).isExpired());
try {
sc.checkAuthenticationAllowed(auth2);
fail("Should have thrown ConcurrentLoginException");
} catch (ConcurrentLoginException expected) {
assertTrue(true);
}
// Attempt to authenticate with a different session for same principal - should expire first session
sc.setExceptionIfMaximumExceeded(false);
Authentication auth3 = createAuthentication("bob", "1212");
sc.checkAuthenticationAllowed(auth3);
sc.registerSuccessfulAuthentication(auth3);
String sessionId3 = ((SessionIdentifierAware) auth3.getDetails()).getSessionId();
assertTrue(registry.getSessionInformation(sessionId1).isExpired());
assertFalse(registry.getSessionInformation(sessionId3).isExpired());
}
@Test(expected=IllegalArgumentException.class)
public void startupDetectsInvalidMaximumSessions() throws Exception {
ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl();
sc.setMaximumSessions(0);
sc.afterPropertiesSet();
}
@Test(expected=IllegalArgumentException.class)
public void startupDetectsInvalidSessionRegistry() throws Exception {
ConcurrentSessionControllerImpl sc = new ConcurrentSessionControllerImpl();
sc.setSessionRegistry(null);
sc.afterPropertiesSet();
}
}
@@ -0,0 +1,50 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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.security.authentication.concurrent;
import junit.framework.TestCase;
import java.util.Date;
import org.springframework.security.authentication.concurrent.SessionInformation;
/**
* Tests {@link SessionInformation}.
*
* @author Ben Alex
* @version $Id$
*/
public class SessionInformationTests extends TestCase {
//~ Methods ========================================================================================================
public void testObject() throws Exception {
Object principal = "Some principal object";
String sessionId = "1234567890";
Date currentDate = new Date();
SessionInformation info = new SessionInformation(principal, sessionId, currentDate);
assertEquals(principal, info.getPrincipal());
assertEquals(sessionId, info.getSessionId());
assertEquals(currentDate, info.getLastRequest());
Thread.sleep(1000);
info.refreshLastRequest();
assertTrue(info.getLastRequest().after(currentDate));
}
}
@@ -0,0 +1,182 @@
/* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
*
* 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.security.authentication.concurrent;
import static org.junit.Assert.*;
import java.util.Date;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.session.SessionDestroyedEvent;
/**
* Tests {@link SessionRegistryImpl}.
*
* @author Ben Alex
* @version $Id$
*/
public class SessionRegistryImplTests {
private SessionRegistryImpl sessionRegistry;
//~ Methods ========================================================================================================
@Before
public void setUp() throws Exception {
sessionRegistry = new SessionRegistryImpl();
}
@Test
public void sessionDestroyedEventRemovesSessionFromRegistry() {
Object principal = "Some principal object";
final String sessionId = "zzzz";
// Register new Session
sessionRegistry.registerNewSession(sessionId, principal);
// De-register session via an ApplicationEvent
sessionRegistry.onApplicationEvent(new SessionDestroyedEvent("") {
@Override
public String getId() {
return sessionId;
}
@Override
public SecurityContext getSecurityContext() {
return null;
}
});
// Check attempts to retrieve cleared session return null
assertNull(sessionRegistry.getSessionInformation(sessionId));
}
@Test
public void testMultiplePrincipals() throws Exception {
Object principal1 = "principal_1";
Object principal2 = "principal_2";
String sessionId1 = "1234567890";
String sessionId2 = "9876543210";
String sessionId3 = "5432109876";
sessionRegistry.registerNewSession(sessionId1, principal1);
sessionRegistry.registerNewSession(sessionId2, principal1);
sessionRegistry.registerNewSession(sessionId3, principal2);
assertEquals(principal1, sessionRegistry.getAllPrincipals()[0]);
assertEquals(principal2, sessionRegistry.getAllPrincipals()[1]);
}
@Test
public void testSessionInformationLifecycle() throws Exception {
Object principal = "Some principal object";
String sessionId = "1234567890";
// Register new Session
sessionRegistry.registerNewSession(sessionId, principal);
// Retrieve existing session by session ID
Date currentDateTime = sessionRegistry.getSessionInformation(sessionId).getLastRequest();
assertEquals(principal, sessionRegistry.getSessionInformation(sessionId).getPrincipal());
assertEquals(sessionId, sessionRegistry.getSessionInformation(sessionId).getSessionId());
assertNotNull(sessionRegistry.getSessionInformation(sessionId).getLastRequest());
// Retrieve existing session by principal
assertEquals(1, sessionRegistry.getAllSessions(principal, false).length);
// Sleep to ensure SessionRegistryImpl will update time
Thread.sleep(1000);
// Update request date/time
sessionRegistry.refreshLastRequest(sessionId);
Date retrieved = sessionRegistry.getSessionInformation(sessionId).getLastRequest();
assertTrue(retrieved.after(currentDateTime));
// Check it retrieves correctly when looked up via principal
assertEquals(retrieved, sessionRegistry.getAllSessions(principal, false)[0].getLastRequest());
// Clear session information
sessionRegistry.removeSessionInformation(sessionId);
// Check attempts to retrieve cleared session return null
assertNull(sessionRegistry.getSessionInformation(sessionId));
assertNull(sessionRegistry.getAllSessions(principal, false));
}
@Test
public void testTwoSessionsOnePrincipalExpiring() throws Exception {
Object principal = "Some principal object";
String sessionId1 = "1234567890";
String sessionId2 = "9876543210";
sessionRegistry.registerNewSession(sessionId1, principal);
SessionInformation[] sessions = sessionRegistry.getAllSessions(principal, false);
assertEquals(1, sessions.length);
assertTrue(contains(sessionId1, principal));
sessionRegistry.registerNewSession(sessionId2, principal);
sessions = sessionRegistry.getAllSessions(principal, false);
assertEquals(2, sessions.length);
assertTrue(contains(sessionId2, principal));
// Expire one session
SessionInformation session = sessionRegistry.getSessionInformation(sessionId2);
session.expireNow();
// Check retrieval still correct
assertTrue(sessionRegistry.getSessionInformation(sessionId2).isExpired());
assertFalse(sessionRegistry.getSessionInformation(sessionId1).isExpired());
}
@Test
public void testTwoSessionsOnePrincipalHandling() throws Exception {
Object principal = "Some principal object";
String sessionId1 = "1234567890";
String sessionId2 = "9876543210";
sessionRegistry.registerNewSession(sessionId1, principal);
SessionInformation[] sessions = sessionRegistry.getAllSessions(principal, false);
assertEquals(1, sessions.length);
assertTrue(contains(sessionId1, principal));
sessionRegistry.registerNewSession(sessionId2, principal);
sessions = sessionRegistry.getAllSessions(principal, false);
assertEquals(2, sessions.length);
assertTrue(contains(sessionId2, principal));
sessionRegistry.removeSessionInformation(sessionId1);
sessions = sessionRegistry.getAllSessions(principal, false);
assertEquals(1, sessions.length);
assertTrue(contains(sessionId2, principal));
sessionRegistry.removeSessionInformation(sessionId2);
assertNull(sessionRegistry.getSessionInformation(sessionId2));
assertNull(sessionRegistry.getAllSessions(principal, false));
}
private boolean contains(String sessionId, Object principal) {
SessionInformation[] info = sessionRegistry.getAllSessions(principal, false);
for (int i = 0; i < info.length; i++) {
if (sessionId.equals(info[i].getSessionId())) {
return true;
}
}
return false;
}
}