Once a SecurityFilterChain is selected, how are its filters actually invoked? Explain VirtualFilterChain.
answer
- VirtualFilterChain = inner FilterChain impl
- seeded with original chain + security filters
- each filter calls chain.doFilter to advance
- not calling doFilter = short-circuit/block
- after last filter -> original container chain -> controller
basics
~10 sFilterChainProxy wraps the selected chain's filters in a VirtualFilterChain. Each filter calls chain.doFilter(...) to advance; after the last security filter, it delegates back to the container's original FilterChain to reach the controller.
solid answer
~40 sAfter selecting a chain, FilterChainProxy creates an inner VirtualFilterChain seeded with the original container FilterChain plus the chain's ordered List<Filter>. It invokes the first security filter with itself as the chain; each filter does its work and calls virtualChain.doFilter(request, response) to pass control to the next security filter. When the security filters are exhausted, VirtualFilterChain delegates to the original container FilterChain, so the request continues to the servlet/DispatcherServlet. This is a linked-list-style traversal: any filter can short-circuit by NOT calling doFilter (e.g. an authentication filter that sends a redirect or 401), which stops the request before it reaches the controller. It's how ordering and early termination both work.
code
java · 15 lines// A custom filter inside a SecurityFilterChain must call chain.doFilter to continue.
public class ApiKeyFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
String key = req.getHeader("X-Api-Key");
if (!isValid(key)) {
res.sendError(HttpServletResponse.SC_UNAUTHORIZED); // short-circuit: no chain.doFilter
return;
}
chain.doFilter(req, res); // advance to the next security filter (VirtualFilterChain)
}
private boolean isValid(String k) { return k != null && !k.isBlank(); }
}
// Registered with: http.addFilterBefore(new ApiKeyFilter(), AuthorizationFilter.class)go deeper
Know filters run in order and one can stop the request.
Explain that each filter calls chain.doFilter to advance and can short-circuit.
Describe VirtualFilterChain construction, delegation back to the original chain, and OncePerRequestFilter.
Reason about custom filter placement, short-circuit semantics, and failure modes (dropped requests, async dispatch).
## What VirtualFilterChain is `VirtualFilterChain` is a private static inner class of `FilterChainProxy`. It implements `jakarta.servlet.FilterChain`. Its job is to sequence the **security** filters of the selected `SecurityFilterChain`, then hand back to the container's real chain. ### How it's constructed When a chain is selected, FilterChainProxy builds a `VirtualFilterChain` with: - `originalChain` — the container's real `FilterChain` (what continues to DispatcherServlet/controller), - `additionalFilters` — the `List<Filter>` from `securityChain.getFilters()`, - an internal position counter starting at 0. Then it calls `virtualFilterChain.doFilter(request, response)`. ### The traversal (simplified) ```java public void doFilter(ServletRequest req, ServletResponse res) { if (this.currentPosition == this.size) { // all security filters done -> continue container chain this.originalChain.doFilter(req, res); return; } this.currentPosition++; Filter next = this.additionalFilters.get(this.currentPosition - 1); next.doFilter(req, res, this); // pass THIS as the chain } ``` Because each security filter receives the `VirtualFilterChain` as its `FilterChain` argument, calling `chain.doFilter(...)` advances to the next security filter. It's cooperative, linked-list-style recursion. ### Short-circuiting (why it matters) A filter advances the request only if it **calls** `chain.doFilter(...)`. If it doesn't: - `UsernamePasswordAuthenticationFilter` on a successful login writes a redirect and returns **without** calling doFilter — the request stops there. - An entry point / exception-translation path can send a 401/redirect and stop. - `AuthorizationFilter` (final authorization) throws `AccessDeniedException` instead of continuing. This is precisely how security filters block requests: they choose not to continue the chain. ### Ordering within a chain The `List<Filter>` order is fixed and meaningful — e.g. `SecurityContextHolderFilter` (loads the SecurityContext) runs before `AuthorizationFilter` (needs the authentication to authorize). Spring assigns each built-in filter a well-defined position; custom filters are inserted via `addFilterBefore` / `addFilterAfter` / `addFilterAt`. ### Gotchas - A filter that forgets to call `chain.doFilter` (in a custom filter) silently drops the request — a common bug. - The original container `FilterChain` is only invoked **after** all security filters pass; anything you place after `FilterChainProxy` in the container (rare) runs after security. - `OncePerRequestFilter` (base class of most Spring Security filters) ensures a filter runs at most once per request even across forwards/includes.
- What happens if a custom filter forgets to call chain.doFilter on the happy path?The request is silently dropped at that filter — it never reaches later security filters or the controller, and often the client gets an empty/committed response. It's a common bug in hand-written filters.
- Why do most Spring Security filters extend OncePerRequestFilter?OncePerRequestFilter guarantees the filter's logic executes at most once per request, even when the request is dispatched again (forwards/includes/async), avoiding duplicate authentication/authorization work.
saying these in an interview costs you the question
- Saying filters are invoked by the container directly (they're driven by VirtualFilterChain inside FilterChainProxy).
- Claiming a filter always continues the chain (it can short-circuit by not calling doFilter).
- Thinking the controller runs before the security filters finish.