skip to content

How does CompositeLogoutHandler work, and how do you add custom logout cleanup (e.g. audit logging or token denylisting)?

level: seniorimportance: should knowfreq 30%

answer

  1. CompositeLogoutHandler loops children in order
  2. LogoutHandler = single method, lambda-able
  3. addLogoutHandler(...) for custom cleanup
  4. capture Authentication before session invalidation
  5. denylist jti / audit log use cases

basics

~20 s

The logout() DSL collects all configured LogoutHandlers into a CompositeLogoutHandler, which invokes each in registration order. You add your own cleanup with addLogoutHandler(myHandler) — for example to write an audit log or add a JWT to a denylist.

solid answer

~40 s

A LogoutFilter can run many LogoutHandlers, so the DSL wraps them in a CompositeLogoutHandler. Composite simply loops over its child handlers and calls logout() on each, in order. The default children include SecurityContextLogoutHandler, LogoutSuccessEventPublishingLogoutHandler, plus CookieClearingLogoutHandler and the RememberMeServices handler when configured. You register your own with .addLogoutHandler(handler); the LogoutHandler interface has one method — logout(request, response, authentication) — so a lambda works. Ordering matters: because SecurityContextLogoutHandler invalidates the session, any handler that reads session state or the Authentication must run before it, so add such handlers early or rely on the passed-in Authentication argument. Typical custom handlers: audit logging, clearing app-specific cookies, adding a JWT jti to a Redis denylist, or notifying downstream systems.

code

java · 17 lines
java
@Bean
SecurityFilterChain security(HttpSecurity http, TokenDenylist denylist) throws Exception {
    http.logout(logout -> logout
        // 1) custom cleanup: audit + denylist the current token
        .addLogoutHandler((request, response, authentication) -> {
            if (authentication != null) {
                String jti = ((JwtAuthenticationToken) authentication).getToken().getId();
                denylist.add(jti);                 // reject this JWT going forward
                log.info("logout user={}", authentication.getName());
            }
        })
        // 2) default SecurityContextLogoutHandler still clears context + session
        .invalidateHttpSession(true)
        .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler()));
    return http.build();
}
// Internally these become children of a CompositeLogoutHandler invoked in order.

go deeper

for a junior

Know you can add extra logout cleanup via addLogoutHandler.

for a middle

Explain that the DSL wraps handlers in a CompositeLogoutHandler run in order.

for a senior

Reason about ordering vs session invalidation, exception propagation, and real cleanup use cases like denylisting.

for a principal

Design idempotent, failure-tolerant logout cleanup and integrate with token denylist / SSO backchannel logout.

## Why a composite Logout is rarely a single action — you clear the context, invalidate the session, drop cookies, cancel remember-me, publish an event, and maybe run app-specific cleanup. Each of those is a **`LogoutHandler`**. To run several from one filter, Spring wraps them in **`CompositeLogoutHandler`**, which itself implements `LogoutHandler`: ```java public void logout(HttpServletRequest req, HttpServletResponse res, Authentication auth) { for (LogoutHandler handler : this.logoutHandlers) { handler.logout(req, res, auth); } } ``` No magic — a simple ordered fan-out. The `logout()` DSL builds this composite from the default handlers plus whatever you add. ## The LogoutHandler contract ```java @FunctionalInterface public interface LogoutHandler { void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication); } ``` One method, so you can pass a lambda. The `authentication` argument is the **currently logged-in principal at the moment of logout** — capture what you need from it here, because after `SecurityContextLogoutHandler` runs the context is cleared. ## Adding your own ```java http.logout(logout -> logout .addLogoutHandler((request, response, authentication) -> { if (authentication != null) { log.info("user {} logged out", authentication.getName()); } }) ); ``` Common real-world handlers: - **Audit logging** — record who logged out and when. - **JWT / token denylist** — extract the token's `jti` and store it in Redis with a TTL equal to the token's remaining lifetime, so a still-valid JWT is rejected after logout. - **App cookie cleanup** — clear cookies Spring doesn't know about. - **Downstream notification** — invalidate a session in an external cache or SSO backchannel. ## Ordering: the key senior insight Handlers run in the order registered. `SecurityContextLogoutHandler` (default) **invalidates the HttpSession**. Therefore: - A handler that needs **session attributes** must run **before** session invalidation. The DSL appends your `addLogoutHandler` handlers relative to the defaults — verify order, or don't depend on the session at all. - A handler that only needs the **principal** is safe anywhere, because the `Authentication` is passed as an argument regardless of whether the context was cleared. If you need precise control, you can build a `CompositeLogoutHandler` yourself and pass it, or provide handlers in the exact order you want. ## Composite vs success handler A `CompositeLogoutHandler` groups **cleanup** handlers (all run). The single **`LogoutSuccessHandler`** runs afterward to write the response. Don't try to write the HTTP response from a `LogoutHandler`; that is the success handler's job. ## Exceptions If a child handler throws, the composite propagates it and later handlers don't run — so keep custom handlers defensive (catch and log rather than break the logout flow) if partial cleanup would leave the user unable to log out. ## Testing With MockMvc: `mockMvc.perform(post("/logout").with(csrf())).andExpect(...)` and verify your handler's side effect (e.g. the denylist entry). Or unit-test the handler directly by calling `logout(req, res, auth)` with mocks.

  • Your custom handler needs a value stored in the HttpSession. What ordering concern applies?
    SecurityContextLogoutHandler invalidates the session, so your handler must run before it to read session attributes. Either register your handler earlier, or design it to rely only on the Authentication argument, which is passed regardless of session state.
  • A LogoutHandler throws an exception mid-chain. What happens to the remaining handlers?
    CompositeLogoutHandler propagates the exception and does not invoke the remaining handlers, so cleanup can be partial. Custom handlers that might fail should catch and log internally so they don't break the overall logout flow.

saying these in an interview costs you the question

  • Thinking a LogoutHandler should write the HTTP response (that is the LogoutSuccessHandler's role).
  • Assuming handlers run in parallel or unordered.
  • Reading session attributes in a handler that runs after session invalidation.
  • Believing there can be only one LogoutHandler.

context