소스 검색

SEC-152: Strategy pattern for SecurityContextHoldder.

Ben Alex 19 년 전
부모
커밋
b05709df6a

+ 95 - 37
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.
  * 
  * <p>
- * To guarantee that {@link #getContext()} never returns <code>null</code>, this
- * class defaults to returning <code>SecurityContextImpl</code> if no
- * <code>SecurityContext</code> has ever been associated with the current
- * thread of execution. Despite this behaviour, in general another class will
- * select the concrete <code>SecurityContext</code> implementation to use and
- * expressly set an instance of that implementation against the
- * <code>SecurityContextHolder</code>.
+ * 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 <code>static</code> to facilitate ease of use
+ * in calling code.
+ * </p>
+ * 
+ * <p>
+ * To specify which strategy should be used, you must provide a mode setting. A
+ * mode setting is one of the three valid <code>MODE_</code> settings defined
+ * as <code>static final</code> fields, or a fully qualified classname to a
+ * concrete implementation of {@link
+ * org.acegisecurity.context.SecurityContextHolderStrategy} that provides a
+ * public no-argument constructor.
+ * </p>
+ * 
+ * <p>
+ * There are two ways to specify the desired mode <code>String</code>. 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).
  * </p>
  *
  * @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 <code>SecurityContext</code>.
+     *
+     * @return the security context (never <code>null</code>)
+     */
+    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 <code>SecurityContext</code> with the current thread of
      * execution.
@@ -53,38 +125,24 @@ public class SecurityContextHolder {
      *        <code>null</code>)
      */
     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 <code>SecurityContext</code> associated with the current
-     * thread of execution. If no <code>SecurityContext</code> 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 <em>NOT</em> 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 <code>SecurityContext</code> (guaranteed to never be
-     *         <code>null</code>)
+     * @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 + "']";
     }
 }

+ 54 - 0
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.
+ * 
+ * <p>
+ * The preferred strategy is loaded by {@link
+ * org.acegisecurity.context.SecurityContextHolder}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface SecurityContextHolderStrategy {
+    //~ Methods ================================================================
+
+    /**
+     * Clears the current context.
+     */
+    public void clearContext();
+
+    /**
+     * Obtains the current context.
+     *
+     * @return a context (never <code>null</code> - create a default
+     *         implementation if necessary)
+     */
+    public SecurityContext getContext();
+
+    /**
+     * Sets the current context.
+     *
+     * @param context to the new argument (should never be <code>null</code>,
+     *        although implementations must check if <code>null</code> has
+     *        been passed and throw an <code>IllegalArgumentException</code>
+     *        in such cases)
+     */
+    public void setContext(SecurityContext context);
+}

+ 180 - 3
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;
+    }
 }