Carrying the user across threads

Spring Security · lesson 17 of 31 · 3 min read

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 hub

Key points

  • SecurityContextHolder is a ThreadLocal. Only the thread that ran the filter chain can read what is in it.
  • A pooled @Async thread starts with an empty context, so getAuthentication() returns null there.
  • Wrap the pool: DelegatingSecurityContextExecutor copies the calling context onto the borrowed thread.
  • Use MODE_INHERITABLETHREADLOCAL only 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.