Spring Security: Passing Security_Context Across Threads
In Spring Security, the SecurityContext is a central component for managing authentication and authorization. It stores security-related information, such as the authenticated user, roles, and authentication tokens. By default, it uses a ThreadLocal-based holding strategy, which ties the SecurityContext to the current thread. While this works well in single-threaded applications, challenges arise when you need to pass the SecurityContext across threads in multi-threaded or asynchronous scenarios.
I tried in this article explains how the holding strategy works, how to pass the SecurityContext to different threads, and the special utilities Spring Security provides to make this easier.
The security context of Spring Security is described by the SecurityContext
interface. The following listing defines this interface :
public interface SecurityContext extends Serializable {
Authentication getAuthentication();
void setAuthentication(Authentication authentication);
}
As you can observe from the contract definition, the primary responsibility of the SecurityContext is to store the Authentication object. But how is the SecurityContext itself managed? Spring Security offers three strategies to manage the SecurityContext with an object in the role of a manager. It’s named the SecurityContextHolder :
Default Holding Strategy for SecurityContext
The SecurityContextHolder is the main utility for storing and accessing the SecurityContext. By default, it uses a ThreadLocal to isolate the context for each thread, When a user logs in, Spring Security stores the SecurityContext in this ThreadLocal, making it available only to the current thread. At the end of the request, the SecurityContext is cleared to avoid memory leaks:
SecurityContextHolder.clearContext();
Challenges in Multi-threaded Applications
In multi-threaded environments, the ThreadLocal strategy becomes problematic because:
Without proper propagation, the SecurityContext is unavailable in new threads, leading to NullPointerException or security violations.
Make sample to understand main problem :
@GetMapping("/hello")
public String hello() {
SecurityContext context = SecurityContextHolder.getContext();
Authentication a = context.getAuthentication();
return "Hello, " + a.getName() + "!";
}// get name in context properly
//or do like this :
@GetMapping("/hello")
public String hello(Authentication a) {
return "Hello, " + a.getName() + "!";
}
//curl -u user:99ff79e3-8ca0-401c-a396-0a8625ab3bad http://localhost:8080/hello
Hello, user!
But when we have method call in different Thread problem raised like this :
@GetMapping("/bye")
@Async
public void goodbye() {
SecurityContext context = SecurityContextHolder.getContext();
String username = context.getAuthentication().getName();
// now error
}
If you try the code as it is now, it throws a NullPointerException on the line that gets the name from the authentication, which is
String username = context.getAuthentication().getName()
This is because the method executes now on another thread that does not inherit the security context. For this reason, the Authorization object is null and, in the context of the presented code, causes a NullPointerException.
Strategies to Pass SecurityContext Across Threads
To address this, Spring Security provides several strategies and utilities to propagate the SecurityContext across threads:
1. Use MODE_INHERITABLETHREADLOCAL
If you need to propagate the SecurityContext to child threads, you can switch to an inheritable thread-local strategy:
@Configuration
@EnableAsync
public class ProjectConfig {
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
}
This ensures that child threads inherit the SecurityContext of the parent thread. For example, this is useful when a thread spawns another thread for background processing:
SecurityContext context = SecurityContextHolder.getContext();
Thread childThread = new Thread(() -> {
System.out.println(SecurityContextHolder.getContext().getAuthentication().getName());
});
childThread.start();
2. MODE_GLOBAL
In rare cases, you may want a global, shared SecurityContext, where all threads share the same context:
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_GLOBAL);
This makes the SecurityContext available to all threads globally. However, it breaks thread isolation and is not recommended for concurrent applications.
3. Custom Holding Strategies
For advanced use cases, you can implement a custom holding strategy by creating a class that implements the SecurityContextHolderStrategy interface. This allows you to define exactly how and where the SecurityContext is stored.
Example :
Recommended by LinkedIn
public class CustomSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final Map<String, SecurityContext> contextMap = new ConcurrentHashMap<>();
@Override
public void clearContext() {
contextMap.remove(Thread.currentThread().getName());
}
@Override
public SecurityContext getContext() {
return contextMap.getOrDefault(Thread.currentThread().getName(), SecurityContextHolder.createEmptyContext());
}
@Override
public void setContext(SecurityContext context) {
contextMap.put(Thread.currentThread().getName(), context);
}
@Override
public SecurityContext createEmptyContext() {
return new SecurityContextImpl();
}
}
You can configure Spring Security to use this strategy:
SecurityContextHolder.setStrategyName("fully.qualified.class.name.of.CustomSecurityContextHolderStrategy");
4. Manual Propagation of SecurityContext
If you cannot use Spring's utilities, you can manually capture and propagate the SecurityContext to other threads. This involves capturing the context in the parent thread and restoring it in the child thread.
Example:
SecurityContext context = SecurityContextHolder.getContext();
Runnable task = () -> {
SecurityContextHolder.setContext(context); // Restore context
try {
// Perform secured operations
System.out.println(SecurityContextHolder.getContext().getAuthentication().getName());
} finally {
SecurityContextHolder.clearContext(); // Clear context to avoid leaks
}
};
// Execute in a new thread
new Thread(task).start();
5. Using DelegatingSecurityContextRunnable or DelegatingSecurityContextCallable
For lightweight use cases, Spring Security provides DelegatingSecurityContextRunnable and DelegatingSecurityContextCallable to wrap Runnable or Callable tasks with the SecurityContext.
Example:
import org.springframework.security.concurrent.DelegatingSecurityContextRunnable;
SecurityContext context = SecurityContextHolder.getContext();
Runnable task = () -> {
// Task logic with SecurityContext
System.out.println(SecurityContextHolder.getContext().getAuthentication().getName());
};
// Wrap the task
Runnable secureTask = new DelegatingSecurityContextRunnable(task, context);
// Run the task in a new thread
new Thread(secureTask).start();
6. DelegatingSecurityContextExecutor/ExecutorService
Spring Security offers DelegatingSecurityContextExecutor and DelegatingSecurityContextExecutorService to automatically propagate the SecurityContext when submitting tasks to an executor.
Example:
import org.springframework.security.concurrent.DelegatingSecurityContextExecutorService;
ExecutorService originalExecutor = Executors.newFixedThreadPool(10);
ExecutorService delegatingExecutor = new DelegatingSecurityContextExecutorService(originalExecutor);
// Submit tasks with propagated SecurityContext
delegatingExecutor.submit(() -> {
// SecurityContext is available in this thread
System.out.println(SecurityContextHolder.getContext().getAuthentication().getName());
});
7. Reactive Applications with Context Propagation
In reactive programming (e.g., using Project Reactor), threads change frequently, making ThreadLocal storage unreliable. Spring Security integrates with Reactor to propagate the SecurityContext using a ReactorContext.
Example:
import reactor.core.publisher.Mono;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
Mono.deferContextual(context -> {
Authentication auth = context.get(Authentication.class); // Retrieve the SecurityContext
return Mono.just(auth.getName());
})
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication)) // Set SecurityContext
.subscribe(System.out::println);
First solution used different Strategy :
@Configuration
@EnableAsync
public class ProjectConfig {
@Bean
public InitializingBean initializingBean() {
return () -> SecurityContextHolder.setStrategyName(
SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
}
Best Practices
Conclusion
Passing the SecurityContext across threads is critical for maintaining authentication and authorization in multi-threaded and asynchronous applications. Spring Security offers several strategies and special utilities, such as DelegatingSecurityContextExecutor and DelegatingSecurityContextRunnable, to simplify context propagation. In reactive applications, Spring Security integrates seamlessly with Reactor to manage context propagation.
By choosing the right strategy and following best practices, you can ensure your application's security context is handled reliably, even in complex threading scenarios.
What strategies do you use to handle SecurityContext in your multi-threaded applications? Share your thoughts in the comments below!
This merged explanation covers the holding strategy and strategies for passing the SecurityContext across threads, offering a complete and professional overview. Let me know if you need further adjustments!
Reference : spring-security-in-action-second-edition-meap-v6
Java Developer at blu bank
3moVery informative.