SEC-152: Strategy pattern for SecurityContextHoldder.
This commit is contained in:
parent
88ff43017d
commit
b05709df6a
|
@ -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 + "']";
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue