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).
*
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 (nevernull
- 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;
+ }
}