Careeroza
SecurityContext and SecurityContextHolder
Where the Logged-in User Lives
SecurityContextHolder (static access point, one per thread)
└── SecurityContext
└── Authentication
├── principal -> UserDetails (username "asha")
├── credentials -> null (erased after login)
└── authorities -> [ROLE_CUSTOMER]By default SecurityContextHolder uses a ThreadLocal: Tomcat handles each request on its own thread, so every request sees its own user, and the holder is cleared when the request ends.
Why a ThreadLocal?
Example
Two requests run at the same time on two threads; each thread sees only its own user:
public class Main {
static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();
static String currentUser() { return CONTEXT.get() == null ? "anonymousUser" : CONTEXT.get(); }
static String handleRequest(String loggedInUser) {
CONTEXT.set(loggedInUser); // done by the security filter
try {
return Thread.currentThread().getName() + " -> controller sees " + currentUser();
} finally {
CONTEXT.remove(); // done by FilterChainProxy after the request
}
}
public static void main(String[] args) throws Exception {
String[] results = new String[2];
Thread t1 = new Thread(() -> results[0] = handleRequest("asha"), "http-nio-8080-exec-1");
Thread t2 = new Thread(() -> results[1] = handleRequest("ravi"), "http-nio-8080-exec-2");
t1.start(); t2.start();
t1.join(); t2.join();
System.out.println(results[0]);
System.out.println(results[1]);
System.out.println("main thread -> " + currentUser());
}
}Output
http-nio-8080-exec-1 -> controller sees asha http-nio-8080-exec-2 -> controller sees ravi main thread -> anonymousUser
Reading the Current User
@RestController
@RequestMapping("/api/me")
public class MeController {
// 1. Inject the principal directly (cleanest)
@GetMapping
public Map<String, Object> me(@AuthenticationPrincipal UserDetails user) {
return Map.of("username", user.getUsername(), "authorities", user.getAuthorities());
}
// 2. Inject the whole Authentication
@GetMapping("/name")
public String name(Authentication authentication) {
return authentication.getName();
}
// 3. Static access, e.g. inside a service
static String currentUsername() {
return SecurityContextHolder.getContext().getAuthentication().getName();
}
}Response
curl -s -u asha:asha123 localhost:8080/api/meExample output
{"username":"asha","authorities":[{"authority":"ROLE_CUSTOMER"}]}Note: @Async methods and new threads do not inherit the ThreadLocal. Use DelegatingSecurityContextExecutor or pass the user id explicitly.