Carrying the user across threads
See why @Async and a plain new Thread lose the current user, and how to hand the context over.
Open this lesson in the learning hubKey points
SecurityContextHolderis a ThreadLocal. Only the thread that ran the filter chain can read what is in it.- A pooled
@Asyncthread starts with an empty context, sogetAuthentication()returnsnullthere. - Wrap the pool:
DelegatingSecurityContextExecutorcopies the calling context onto the borrowed thread. - Use
MODE_INHERITABLETHREADLOCALonly for threads you create yourself - a pooled thread would inherit whoever started it. - Always clear it again. A pooled thread holding a stale context can serve the next request as the wrong user.
- WebFlux does not use ThreadLocals at all: there the context rides in the Reactor context instead.
Example
@Bean
Executor securityAwareExecutor() {
ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
pool.setCorePoolSize(4);
pool.initialize();
return new DelegatingSecurityContextExecutor(pool); // copies the context in
}
@Async
public void audit(String action) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// null on a raw pool thread, "ada" once the executor is wrapped
log.info("{} did {}", auth == null ? "unknown" : auth.getName(), action);
}
The user lives on one thread - hand it over on purpose, or the copy is null.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the Spring Security course, and every lesson in it is listed on the Spring Security contents page.