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/me

Example 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.