diff --git a/core/src/main/java/org/acegisecurity/context/SecurityContextHolder.java b/core/src/main/java/org/acegisecurity/context/SecurityContextHolder.java index de92f66074..5042f57a00 100644 --- a/core/src/main/java/org/acegisecurity/context/SecurityContextHolder.java +++ b/core/src/main/java/org/acegisecurity/context/SecurityContextHolder.java @@ -1,4 +1,4 @@ -/* Copyright 2004, 2005 Acegi Technology Pty Limited +/* 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. @@ -15,7 +15,9 @@ package org.acegisecurity.context; -import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +import java.lang.reflect.Constructor; /** @@ -23,28 +25,98 @@ import org.springframework.util.Assert; * thread. * *

- * To guarantee that {@link #getContext()} never returns null, this - * class defaults to returning SecurityContextImpl if no - * SecurityContext has ever been associated with the current - * thread of execution. Despite this behaviour, in general another class will - * select the concrete SecurityContext implementation to use and - * expressly set an instance of that implementation against the - * SecurityContextHolder. + * This class provides a series of static methods that delegate to an instance + * of {@link org.acegisecurity.context.SecurityContextHolderStrategy}. The + * purpose of the class is to provide a convenient way to specify the strategy + * that should be used for a given JVM. This is a JVM-wide setting, since + * everything in this class is static to facilitate ease of use + * in calling code. + *

+ * + *

+ * To specify which strategy should be used, you must provide a mode setting. A + * mode setting is one of the three valid MODE_ settings defined + * as static final fields, or a fully qualified classname to a + * concrete implementation of {@link + * org.acegisecurity.context.SecurityContextHolderStrategy} that provides a + * public no-argument constructor. + *

+ * + *

+ * There are two ways to specify the desired mode String. The + * first is to specify it via the system property keyed on {@link + * #SYSTEM_PROPERTY}. The second is to call {@link #setStrategyName(String)} + * before using the class. If neither approach is used, the class will default + * to using {@link #MODE_THREADLOCAL}, which is backwards compatible, has + * fewer JVM incompatibilities and is appropriate on servers (whereas {@link + * #MODE_GLOBAL} is not). *

* * @author Ben Alex * @version $Id$ * - * @see java.lang.ThreadLocal * @see org.acegisecurity.context.HttpSessionContextIntegrationFilter */ public class SecurityContextHolder { //~ Static fields/initializers ============================================= - private static ThreadLocal contextHolder = new ThreadLocal(); + public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL"; + public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL"; + public static final String MODE_GLOBAL = "MODE_GLOBAL"; + public static final String SYSTEM_PROPERTY = "acegi.security.strategy"; + private static String strategyName = System.getProperty(SYSTEM_PROPERTY); + private static Constructor customStrategy; + private static SecurityContextHolderStrategy strategy; //~ Methods ================================================================ + /** + * Explicitly clears the context value from the current thread. + */ + public static void clearContext() { + initialize(); + strategy.clearContext(); + } + + /** + * Obtain the current SecurityContext. + * + * @return the security context (never null) + */ + public static SecurityContext getContext() { + initialize(); + + return strategy.getContext(); + } + + private static void initialize() { + if ((strategyName == null) || "".equals(strategyName)) { + // Set default + strategyName = MODE_THREADLOCAL; + } + + if (strategyName.equals(MODE_THREADLOCAL)) { + strategy = new ThreadLocalSecurityContextHolderStrategy(); + } else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) { + strategy = new InheritableThreadLocalSecurityContextHolderStrategy(); + } else if (strategyName.equals(MODE_GLOBAL)) { + strategy = new GlobalSecurityContextHolderStrategy(); + } else { + // Try to load a custom strategy + try { + if (customStrategy == null) { + Class clazz = Class.forName(strategyName); + customStrategy = clazz.getConstructor(new Class[] {}); + } + + strategy = (SecurityContextHolderStrategy) customStrategy + .newInstance(new Object[] {}); + } catch (Exception ex) { + ReflectionUtils.handleReflectionException(ex); + } + } + } + /** * Associates a new SecurityContext with the current thread of * execution. @@ -53,38 +125,24 @@ public class SecurityContextHolder { * null) */ public static void setContext(SecurityContext context) { - Assert.notNull(context, - "Only non-null SecurityContext instances are permitted"); - contextHolder.set(context); + initialize(); + strategy.setContext(context); } /** - * Obtains the SecurityContext associated with the current - * thread of execution. If no SecurityContext has been - * associated with the current thread of execution, a new instance of - * {@link SecurityContextImpl} is associated with the current thread and - * then returned. + * Changes the preferred strategy. Do NOT call this method more + * than once for a given JVM, as it will reinitialize the strategy and + * adversely affect any existing threads using the old strategy. * - * @return the current SecurityContext (guaranteed to never be - * null) + * @param strategyName the fully qualified classname of the strategy that + * should be used. */ - public static SecurityContext getContext() { - if (contextHolder.get() == null) { - contextHolder.set(new SecurityContextImpl()); - } - - return (SecurityContext) contextHolder.get(); + public static void setStrategyName(String strategyName) { + SecurityContextHolder.strategyName = strategyName; + initialize(); } - /** - * Explicitly clears the context value from thread local storage. - * Typically used on completion of a request to prevent potential - * misuse of the associated context information if the thread is - * reused. - */ - public static void clearContext() { - // Internally set the context value to null. This is never visible - // outside the class. - contextHolder.set(null); + public String toString() { + return "SecurityContextHolder[strategy='" + strategyName + "']"; } } diff --git a/core/src/main/java/org/acegisecurity/context/SecurityContextHolderStrategy.java b/core/src/main/java/org/acegisecurity/context/SecurityContextHolderStrategy.java new file mode 100644 index 0000000000..319ad94633 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/context/SecurityContextHolderStrategy.java @@ -0,0 +1,54 @@ +/* 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.acegisecurity.context; + +/** + * A strategy for storing security context information against a thread. + * + *

+ * The preferred strategy is loaded by {@link + * org.acegisecurity.context.SecurityContextHolder}. + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public interface SecurityContextHolderStrategy { + //~ Methods ================================================================ + + /** + * Clears the current context. + */ + public void clearContext(); + + /** + * Obtains the current context. + * + * @return a context (never null - create a default + * implementation if necessary) + */ + public SecurityContext getContext(); + + /** + * Sets the current context. + * + * @param context to the new argument (should never be null, + * although implementations must check if null has + * been passed and throw an IllegalArgumentException + * in such cases) + */ + public void setContext(SecurityContext context); +} diff --git a/core/src/test/java/org/acegisecurity/context/SecurityContextHolderTests.java b/core/src/test/java/org/acegisecurity/context/SecurityContextHolderTests.java index 4067441942..d3694b6c33 100644 --- a/core/src/test/java/org/acegisecurity/context/SecurityContextHolderTests.java +++ b/core/src/test/java/org/acegisecurity/context/SecurityContextHolderTests.java @@ -15,6 +15,11 @@ package org.acegisecurity.context; +import java.util.Random; + +import org.acegisecurity.providers.UsernamePasswordAuthenticationToken; + +import junit.framework.ComparisonFailure; import junit.framework.TestCase; @@ -26,6 +31,7 @@ import junit.framework.TestCase; */ public class SecurityContextHolderTests extends TestCase { //~ Constructors =========================================================== + private static int errors = 0; public SecurityContextHolderTests() { super(); @@ -38,23 +44,28 @@ public class SecurityContextHolderTests extends TestCase { //~ Methods ================================================================ public final void setUp() throws Exception { - super.setUp(); + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); } public static void main(String[] args) { junit.textui.TestRunner.run(SecurityContextHolderTests.class); } - public void testContextHolderGetterSetter() { + public void testContextHolderGetterSetterClearer() { SecurityContext sc = new SecurityContextImpl(); + sc.setAuthentication(new UsernamePasswordAuthenticationToken("Foobar","pass")); SecurityContextHolder.setContext(sc); assertEquals(sc, SecurityContextHolder.getContext()); + SecurityContextHolder.clearContext(); + assertNotSame(sc, SecurityContextHolder.getContext()); + SecurityContextHolder.clearContext(); } public void testNeverReturnsNull() { assertNotNull(SecurityContextHolder.getContext()); + SecurityContextHolder.clearContext(); } - + public void testRejectsNulls() { try { SecurityContextHolder.setContext(null); @@ -63,4 +74,170 @@ public class SecurityContextHolderTests extends TestCase { assertTrue(true); } } + + public void testSynchronizationCustomStrategyLoading() { + SecurityContextHolder.setStrategyName(InheritableThreadLocalSecurityContextHolderStrategy.class.getName()); + assertEquals("SecurityContextHolder[strategy='org.acegisecurity.context.InheritableThreadLocalSecurityContextHolderStrategy']", new SecurityContextHolder().toString()); + loadStartAndWaitForThreads(true, "Main_", 10, false, true); + assertEquals("Thread errors detected; review log output for details", 0, errors); + } + + public void testSynchronizationInheritableThreadLocal() throws Exception { + SecurityContextHolder.clearContext(); + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); + loadStartAndWaitForThreads(true, "Main_", 10, false, true); + assertEquals("Thread errors detected; review log output for details", 0, errors); + } + + public void testSynchronizationThreadLocal() throws Exception { + SecurityContextHolder.clearContext(); + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_THREADLOCAL); + loadStartAndWaitForThreads(true, "Main_", 10, false, false); + assertEquals("Thread errors detected; review log output for details", 0, errors); + } + + public void testSynchronizationGlobal() throws Exception { + SecurityContextHolder.clearContext(); + SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL); + loadStartAndWaitForThreads(true, "Main_", 10, true, false); + assertEquals("Thread errors detected; review log output for details", 0, errors); + } + + private void loadStartAndWaitForThreads(boolean topLevelThread, String prefix, int createThreads, boolean expectAllThreadsToUseIdenticalAuthentication, boolean expectChildrenToShareAuthenticationWithParent) { + Thread[] threads = new Thread[createThreads]; + errors = 0; + + if (topLevelThread) { + // PARENT (TOP-LEVEL) THREAD CREATION + if (expectChildrenToShareAuthenticationWithParent) { + // An InheritableThreadLocal + for (int i = 0; i < threads.length; i++) { + if (i % 2 == 0) { + // Don't inject auth into current thread; neither current thread or child will have authentication + threads[i] = makeThread(prefix + "Unauth_Parent_" + i, true, false, false, true, null); + } else { + // Inject auth into current thread, but not child; current thread will have auth, child will also have auth + threads[i] = makeThread(prefix + "Auth_Parent_" + i, true, true, false, true, prefix + "Auth_Parent_" + i); + } + } + } else if (expectAllThreadsToUseIdenticalAuthentication) { + // A global + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("GLOBAL_USERNAME","pass")); + for (int i = 0; i < threads.length; i++) { + if (i % 2 == 0) { + // Don't inject auth into current thread;both current thread and child will have same authentication + threads[i] = makeThread(prefix + "Unauth_Parent_" + i, true, false, true, true, "GLOBAL_USERNAME"); + } else { + // Inject auth into current thread; current thread will have auth, child will also have auth + threads[i] = makeThread(prefix + "Auth_Parent_" + i, true, true, true, true, "GLOBAL_USERNAME"); + } + } + } else { + // A standard ThreadLocal + for (int i = 0; i < threads.length; i++) { + if (i % 2 == 0) { + // Don't inject auth into current thread; neither current thread or child will have authentication + threads[i] = makeThread(prefix + "Unauth_Parent_" + i, true, false, false, false, null); + } else { + // Inject auth into current thread, but not child; current thread will have auth, child will not have auth + threads[i] = makeThread(prefix + "Auth_Parent_" + i, true, true, false, false, prefix + "Auth_Parent_" + i); + } + } + } + } else { + // CHILD THREAD CREATION + if (expectChildrenToShareAuthenticationWithParent || expectAllThreadsToUseIdenticalAuthentication) { + // The children being created are all expected to have security (ie an InheritableThreadLocal/global AND auth was injected into parent) + for (int i = 0; i < threads.length; i++) { + String expectedUsername = prefix; + if (expectAllThreadsToUseIdenticalAuthentication) { + expectedUsername = "GLOBAL_USERNAME"; + } + // Don't inject auth into current thread; the current thread will obtain auth from its parent + // NB: As topLevelThread = true, no further child threads will be created + threads[i] = makeThread(prefix + "->child->Inherited_Auth_Child_" + i, false, false, expectAllThreadsToUseIdenticalAuthentication, false, expectedUsername); + } + } else { + // The children being created are NOT expected to have security (ie not an InheritableThreadLocal OR auth was not injected into parent) + for (int i = 0; i < threads.length; i++) { + // Don't inject auth into current thread; neither current thread or child will have authentication + // NB: As topLevelThread = true, no further child threads will be created + threads[i] = makeThread(prefix + "->child->Unauth_Child_" + i, false, false, false, false, null); + } + } + } + + // Start and execute the threads + startAndRun(threads); + } + + private void startAndRun(Thread[] threads) { + // Start them up + for (int i = 0; i < threads.length; i++) { + threads[i].start(); + } + + // Wait for them to finish + while (stillRunning(threads)) { + try { + Thread.sleep(250); + } catch (InterruptedException ignore) {} + } + } + + private boolean stillRunning(Thread[] threads) { + for (int i = 0; i < threads.length; i++) { + if (threads[i].isAlive()) { + return true; + } + } + return false; + } + + private Thread makeThread(final String threadIdentifier, final boolean topLevelThread, final boolean injectAuthIntoCurrentThread, final boolean expectAllThreadsToUseIdenticalAuthentication, final boolean expectChildrenToShareAuthenticationWithParent, final String expectedUsername) { + final Random rnd = new Random(); + + Thread t = new Thread(new Runnable() { + public void run() { + if (injectAuthIntoCurrentThread) { + // Set authentication in this thread + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(expectedUsername,"pass")); + //System.out.println(threadIdentifier + " - set to " + SecurityContextHolder.getContext().getAuthentication()); + } else { + //System.out.println(threadIdentifier + " - not set (currently " + SecurityContextHolder.getContext().getAuthentication() + ")"); + } + + // Do some operations in current thread, checking authentication is as expected in the current thread (ie another thread doesn't change it) + for (int i = 0; i < 25; i++) { + String currentUsername = SecurityContextHolder.getContext().getAuthentication() == null ? null : SecurityContextHolder.getContext().getAuthentication().getName(); + + if (i % 7 == 0) { + System.out.println(threadIdentifier + " at " + i + " username " + currentUsername); + } + + try { + TestCase.assertEquals("Failed on iteration " + i + "; Authentication was '" + currentUsername + "' but principal was expected to contain username '" + expectedUsername + "'", expectedUsername, currentUsername); + } catch (ComparisonFailure err) { + errors++; + throw err; + } + + try { + Thread.sleep(rnd.nextInt(250)); + } catch (InterruptedException ignore) {} + } + + // Load some children threads, checking the authentication is as expected in the children (ie another thread doesn't change it) + if (topLevelThread) { + // Make four children, but we don't want the children to have any more children (so anti-nature, huh?) + if (injectAuthIntoCurrentThread && expectChildrenToShareAuthenticationWithParent) { + loadStartAndWaitForThreads(false, threadIdentifier, 4, expectAllThreadsToUseIdenticalAuthentication, true); + } else { + loadStartAndWaitForThreads(false, threadIdentifier, 4, expectAllThreadsToUseIdenticalAuthentication, false); + } + } + } + }, threadIdentifier); + return t; + } }