Spring Security and @Async (Authenticated Users mixed up)
SpringAsynchronousSpring SecuritySpring Problem Overview
I asynchronously invoke method with Spring, using @Async.This method invokes other method annotated with @PreAuthorize, Spring Security Annotation. To make authorization works I have to set SecurityContextHolder
mode to MODE_INHERITABLETHREADLOCAL
, so that authentication info is passed to the asynchronous call. Everything works fine so far.
However when I logout and login as a different user, in asynchronous method SecurityContextHolder stores authentication info of the old user, that has bee logged out. It causes of course unwanted AccessDenied
exception. There is no such problem with synchronous calls.
I have defined <task:executor id="executors" pool-size="10"/>
, so may it be a problem that once thread in executors pool has been initialized it will not override authentication information?
Spring Solutions
Solution 1 - Spring
I guess MODE_INHERITABLETHREADLOCAL
doesn't work correctly with thread pool.
As a possible solution you can try to subclass ThreadPoolTaskExecutor
and override its methods to propagate SecurityContext
manually, and then declare that executor instead of <task:executor>
, something like this:
public void execute(final Runnable r) {
final Authentication a = SecurityContextHolder.getContext().getAuthentication();
super.execute(new Runnable() {
public void run() {
try {
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
ctx.setAuthentication(a);
SecurityContextHolder.setContext(ctx);
r.run();
} finally {
SecurityContextHolder.clearContext();
}
}
});
}
Solution 2 - Spring
I also ran into that problem. It is important to configure the ThreadPoolTaskExecutor correctly using the DelegatingSecurityContextAsyncTaskExecutor
. Also it is important to call the initialize() method, otherwise an error is thrown.
// define the TaskExecutor as a bean
@Bean("threadPoolTaskExecutor")
public TaskExecutor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(1000);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("Async-");
executor.initialize(); // this is important, otherwise an error is thrown
return new DelegatingSecurityContextAsyncTaskExecutor(executor); // use this special TaskExecuter
}
// the method in your business logic which is called async
@Override
@Async("threadPoolTaskExecutor")
public void yourLogic() {
[..]
}
Solution 3 - Spring
This is just a hint that needs future investigation (I am too tired, but maybe somebody find this useful for future investigation):
Today I stumbled over org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor
see GitHub.
it looks like that his designed to delegate the security context so that it is "passed" through the @Async
call.
Also have a look at this post: Spring Security 3.2 M1 Highlights, Servlet 3 API Support is sounds like it is strongly related.
Solution 4 - Spring
Using the information from Ralph and Oak -
If you want to get @Async working with the standard task executor tag, you would set up your Spring XML config like this
<task:annotation-driven executor="_importPool"/>
<task:executor id="_importPool" pool-size="5"/>
<bean id="importPool"
class="org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor">
<constructor-arg ref="_importPool"/>
</bean>
Then in your @Async method, you would specify the pool you want to use
@Async("importPool")
public void run(ImportJob import) {
...
}
That should work so when whenever you call your @Async method, the threadpool thread will use the same security context as the calling thread
Solution 5 - Spring
Based on @Ralph answer one can achieve Aync event
with Spring
with threadpooling
and delegate the security using http://docs.spring.io/autorepo/docs/spring-security/4.0.0.M1/apidocs/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.html
Sample code
<bean id="applicationEventMulticaster"
class="org.springframework.context.event.SimpleApplicationEventMulticaster">
<property name="taskExecutor">
<ref bean="delegateSecurityAsyncThreadPool"/>
</property>
</bean>
<bean id="threadsPool"
class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
</bean>
<bean id="delegateSecurityAsyncThreadPool"
class="org.springframework.security.task.DelegatingSecurityContextTaskExecutor">
<constructor-arg ref="threadsPool"/>
</bean>
Solution 6 - Spring
As it was already mentioned, for pooled threads environment DelegatingSecurityContextAsyncTaskExecutor
should be used, instead of MODE_INHERITABLETHREADLOCAL
(read here).
Leaving simple DelegatingSecurityContextAsyncTaskExecutor
configuration for Spring Boot projects which will simply use default Spring Boot pool for async tasks:
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
private final ThreadPoolTaskExecutor defaultSpringBootAsyncExecutor;
public AsyncConfig(ThreadPoolTaskExecutor defaultSpringBootAsyncExecutor) {
this.defaultSpringBootAsyncExecutor = defaultSpringBootAsyncExecutor;
}
@Override
public Executor getAsyncExecutor() {
return new DelegatingSecurityContextAsyncTaskExecutor(defaultSpringBootAsyncExecutor);
}
}
Solution 7 - Spring
Jus to add to the answer from @axtavt, you would also want to override other method.
@Override
public <T> Future<T> submit(Callable<T> task) {
ExecutorService executor = getThreadPoolExecutor();
final Authentication a = SecurityContextHolder.getContext().getAuthentication();
try {
return executor.submit(new Callable<T>() {
@Override
public T call() throws Exception {
try {
SecurityContext ctx = SecurityContextHolder.createEmptyContext();
ctx.setAuthentication(a);
SecurityContextHolder.setContext(ctx);
return task.call();
} catch (Exception e) {
slf4jLogger.error("error invoking async thread. error details : {}", e);
return null;
} finally {
SecurityContextHolder.clearContext();
}
}
});
} catch (RejectedExecutionException ex) {
throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
}
}
Solution 8 - Spring
I use an Enterprise Jboss Server with a JNDI managed thread pool; This is what worked for me:
@Configuration
@EnableAsync
public class AsyncAppConfig {
public static final String THREAD_POOL_ID = "threadPoolId";
@Bean(THREAD_POOL_ID)
public DelegatingSecurityContextAsyncTaskExecutor secureThreadPool(
DefaultManagedTaskExecutor jbossManagedTaskExecutor) {
return new DelegatingSecurityContextAsyncTaskExecutor(jbossManagedTaskExecutor);
}
@Bean
public DefaultManagedTaskExecutor jbossManagedTaskExecutor() {
return new DefaultManagedTaskExecutor() {
@Override
public void afterPropertiesSet() throws NamingException {
// gets the ConcurrentExecutor configured by Jboss
super.afterPropertiesSet();
// Wraps the jboss configured ConcurrentExecutor in a securityContext aware delegate
setConcurrentExecutor(new DelegatingSecurityContextExecutor(getConcurrentExecutor(), getContext()));
}
};
}
}