Spring Security is a linked list, not a black box

by Kaushal Bhatt
4 min read

Spring Security clicks the moment you stop thinking of it as magic and start thinking of it as a linked list. SecurityFilterChain is exactly that — an ordered list of servlet filters. Each one can do three things and no more: inspect the request, mutate the SecurityContext, or short-circuit and return a response before your controller ever runs. Once you hold that picture, most of the framework's surprising behaviour stops being surprising. ## The filters that matter, in the order they run - Header filters set up the response before anything else touches it. - SecurityContextHolderFilter loads any existing security context for this request. - CsrfFilter rejects state-changing requests arriving without a valid token. - Your authentication filter — form login, JWT, OAuth2 — populates the context. - ExceptionTranslationFilter catches AccessDeniedException and AuthenticationException thrown by anything downstream and turns them into a 401, a 403, or a redirect. - AuthorizationFilter, last, evaluates your authorizeHttpRequests rules and decides whether this specific request gets through at all. The last one being last is the whole design. Authorization is not something that happens somewhere in the middle of the chain; it is the final gate before the request reaches your code. ## Order is not a suggestion — it is the mechanism This is the part that trips people up. Put a custom filter before AuthorizationFilter and it runs on every request, regardless of your authorization rules. Put it after and it only ever sees requests that already passed them. That is not a performance detail. It changes what your filter is allowed to assume. A filter registered before authorization runs cannot assume anything about the caller — including that they were permitted to reach this endpoint. Plenty of code written on the assumption that it runs "inside" the security perimeter is in fact sitting outside it. The same trap has a second form:

java
http.addFilterAfter(jwtFilter, UsernamePasswordAuthenticationFilter.class);

That places your JWT filter after form login's filter. Perfectly fine for an API with no form login. Wrong the moment both coexist — the form-login filter has already had its turn at the context, and now your ordering depends on which of the two the request happens to satisfy. ## What to check first Next time a request comes back with a 403 nobody can explain, do not start with the authorization rule. Print the chain and look at where your custom filter actually landed relative to AuthorizationFilter.


logging.level.org.springframework.security=DEBUG

Spring will list the chain, in order, at startup. Nine times out of ten the rule was right and the position was wrong. The general habit: when a framework's behaviour looks like magic, find the ordered list it is really made of. Spring Security has exactly one, and it is printable.