So I'm working on this Spring MVC application using Spring Security. I've been hitting a performance problem in some instances where my controller is taking way too long to respond. This is due to a processing method that can take a huge amount of data in to process, based on some user input.
Now I've been tweaking the code a bit in and around that processing method with my team and I don't think we can get much better performance out of that without slicing it and executing each slice asynchronously.
The problem is when I try to slice it and distribute the work to child threads, using a threadpool from java.util.concurrent, I get error messages about the security context when those execute.
Here is an extract of the stacktrace:
Exception in thread "pool-1-thread-3" org.springframework.security.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
at org.springframework.security.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:342)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:254)
at org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:63)
Exception in thread "pool-1-thread-4" at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
at $Proxy63.batchSaveCampaignpayment(Unknown Source)
at com.fim.pnp.controller.PaymentForm$1.run(PaymentForm.java:224)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)
at java.lang.Thread.run(Thread.java:595)
org.springframework.security.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
at org.springframework.security.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:342)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:254)
at org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:63)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
at $Proxy63.batchSaveCampaignPayment(Unknown Source)
at com.fim.pnp.controller.PaymentForm$1.run(PaymentForm.java:224)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)
at java.lang.Thread.run(Thread.java:595)
Exception in thread "pool-1-thread-5" org.springframework.security.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
at org.springframework.security.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:342)
at org.springframework.security.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:254)
at org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:63)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:106)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:171)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)
at $Proxy63.batchSaveCampaignPayment(Unknown Source)
at com.fim.pnp.controller.PaymentForm$1.run(PaymentForm.java:224)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:650)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:675)
at java.lang.Thread.run(Thread.java:595)
I know it's not good practice to spawn threads from a request... but we've ran out of ideas at this point and each worker thread shouldn't take more than a handful of seconds by our measurements. Also it is expected that this feature is going to be used by 1 or 2 dedicated users once a week only.
Is there a way to pass the securityContext to the child threads or anything similar that would allow those threads to execute?
Thanks
SecurityContextHolder - The SecurityContextHolder is where Spring Security stores the details of who is authenticated. SecurityContext - is obtained from the SecurityContextHolder and contains the Authentication of the currently authenticated user.
Yes, it's thread safe with the default strategy ( MODE_THREADLOCAL ) (as long as you don't try to change the strategy on the fly). However, if you want spawned threads to inherit SecurityContext of the parent thread, you should set MODE_INHERITABLETHREADLOCAL .
AuthenticationManagerBuilder. parentAuthenticationManager(AuthenticationManager authenticationManager) Allows providing a parent AuthenticationManager that will be tried if this AuthenticationManager was unable to attempt to authenticate the provided Authentication . protected ProviderManager.
Any new thread does not get the context information from the parent thread. MODE_INHERITABLETHREADLOCAL : It copies the security context to the next thread in case of an asynchronous method. MODE_GLOBAL : It makes all the threads of the application see the same security context instance.
I found two solutions when I had a similar problem:
Solution 1 Change strategy of SecurityContextHolder to MODE_INHERITABLETHREADLOCAL
You may do it in such way
<beans:bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<beans:property name="targetClass"
value="org.springframework.security.core.context.SecurityContextHolder"/>
<beans:property name="targetMethod" value="setStrategyName"/>
<beans:property name="arguments" value="MODE_INHERITABLETHREADLOCAL"/>
</beans:bean>
Solution 2 Use DelegatingSecurityContextRunnable, DelegatingSecurityContextExecutor or DelegatingSecurityContextExecutor. This soulution is well described in Spring Concurrency Support Documentation
We usually do the following: In the initial spring-managed thread do
Locale locale = LocaleContextHolder.getLocale();
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
Now you need to put those two values somewhere your new thread can find them. Then you do:
LocaleContextHolder.setLocale( locale, true);
RequestContextHolder.setRequestAttributes( ra, true);
In your new thread. Although I'm not sure if this is the supported method, it's always worked well.
Maybe you could use something like InheritableThreadLocalSecurityContextHolderStrategy ? I think what it does is to copy the security context of the current thread to any threads you create within
As other mentioned, in the documentation there is a neat example. Which could be improved by using lambda functions.
SecurityContext securityContext = SecurityContextHolder.getContext();
DelegatingSecurityContextRunnable wrappedRunnable = new DelegatingSecurityContextRunnable(() -> doSomething()), securityContext);
new Thread(wrappedRunnable).start();
Nonetheless, I would also like to point to another solution. I would recommend using the @Async
annotation. You would just need to add the following configuration.
@Bean("contextPropagatedExecutor")
public TaskExecutor contextPropagatedExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setMaxPoolSize(15);
executor.setQueueCapacity(Integer.MAX_VALUE);
executor.setThreadNamePrefix("PropagatedContext-");
executor.initialize();
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
Then, all you would need to do is to add the annotation like followed. This is easier to manage and can be reutilized if needed.
@Async("contextPropagatedExecutor")
public void doSomething(){}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With