skip to content

How do addFilterBefore, addFilterAfter, and addFilterAt work, and when would you use each to insert a custom filter?

level: seniorimportance: should knowfreq 55%

answer

  1. Before/After = deterministic delta; At = same order = undefined tie
  2. Reference class must be in FilterOrderRegistration or build throws
  3. JWT filter: addFilterBefore(UsernamePasswordAuthenticationFilter)
  4. Extend OncePerRequestFilter -> doFilterInternal, call chain
  5. Throwing filter -> put after ExceptionTranslationFilter for clean 401/403

basics

~20 s

They insert your custom filter relative to a known Spring Security filter. addFilterBefore(f, X) runs f before filter X; addFilterAfter(f, X) after X; addFilterAt(f, X) at X's position (order among same-position filters is undefined). Use them because you can't hand-number Spring's built-in filters.

solid answer

~40 s

`HttpSecurity` exposes `addFilterBefore(Filter, Class<? extends Filter>)`, `addFilterAfter(...)`, and `addFilterAt(...)`. You pass your filter plus a *reference* built-in filter class; Spring inserts yours immediately before/after/at that reference's position in `FilterOrderRegistration`. Use `addFilterBefore(myAuthFilter, UsernamePasswordAuthenticationFilter.class)` for a custom authentication filter that must run in the auth slot (e.g. a JWT/API-key filter that populates the context before form login). Use `addFilterAfter` when you need something to run once a prior stage is done. Avoid `addFilterAt` unless necessary: it places your filter at the same order value as the reference, and the relative order of two filters sharing a position is **undefined** — fine only if you're replacing that filter and remove the original. The reference class must be one Spring knows in its order registry; an unknown class throws at build time.

code

java · 22 lines
java
public class ApiKeyAuthFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        String key = req.getHeader("X-Api-Key");
        if (key != null && isValid(key)) {
            var auth = new PreAuthenticatedAuthenticationToken(
                principalFor(key), null, authoritiesFor(key));
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(req, res); // never forget to continue the chain
    }
}

@Bean
SecurityFilterChain chain(HttpSecurity http, ApiKeyAuthFilter apiKeyFilter) throws Exception {
    http
        .authorizeHttpRequests(a -> a.anyRequest().authenticated())
        // Run BEFORE the form-login slot so identity is set for AuthorizationFilter.
        .addFilterBefore(apiKeyFilter, UsernamePasswordAuthenticationFilter.class);
    return http.build();
}

go deeper

for a junior

Know the three methods exist and take a reference filter class.

for a middle

Pick addFilterBefore(UsernamePasswordAuthenticationFilter.class) for a custom auth filter and explain why.

for a senior

Articulate the addFilterAt undefined-order gotcha and the build-time requirement that the reference class be registered.

for a principal

Reason about placement relative to ExceptionTranslationFilter for error semantics and OncePerRequestFilter for dispatch correctness.

## The three DSL methods All live on `HttpSecurity` and return `HttpSecurity` for chaining: - **`addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)`** — insert `filter` immediately *before* the position of `beforeFilter`. - **`addFilterAfter(Filter filter, Class<? extends Filter> afterFilter)`** — insert immediately *after* it. - **`addFilterAt(Filter filter, Class<? extends Filter> atFilter)`** — insert at the *same* order value. ### Why you need them Spring Security's built-in filters have their order fixed by an internal **`FilterOrderRegistration`** (a map of filter class -> integer order step). You cannot assign your own integer to sit between them. These three methods let you express placement *relative* to a known filter, and Spring computes the concrete order. ### How `addFilterAt` differs — the gotcha `addFilterBefore`/`addFilterAfter` give your filter a distinct order value (a small delta before/after the reference), so ordering is deterministic. `addFilterAt` gives your filter the **exact same** order value as the reference filter. If both remain in the chain, their relative order is **undefined** — Spring even documents that it does not guarantee it and won't stop you shooting yourself in the foot. So `addFilterAt` is appropriate only when you intend to *replace* a filter's role and that filter isn't also present (e.g. you disabled the built-in and drop in your own at its slot). ### Requirements on the reference class - The reference `Class` must be a filter Spring's order registry knows about (a built-in, or one already registered). Passing an unregistered class throws `IllegalArgumentException` ("The Filter class ... does not have a registered order") at `http.build()` time. - Your custom filter itself does **not** need to be a subclass of any Spring filter; it just needs to implement `jakarta.servlet.Filter` (commonly by extending `OncePerRequestFilter`). ### Typical placements - **Custom token/JWT/API-key authentication** -> `addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)`. It authenticates and populates `SecurityContextHolder` before the form-login slot, and — crucially — after `SecurityContextHolderFilter` so a context exists, and before `AuthorizationFilter` so authorization sees the identity. - **Request-logging / correlation-id / tenant-resolution** that must see the authenticated user -> `addFilterAfter(auditFilter, AuthorizationFilter.class)` or after an auth filter, depending on whether it needs identity. - **Pre-auth header extraction** feeding a downstream authenticator -> `addFilterBefore(...)` the authenticator. ### `OncePerRequestFilter` Most custom filters extend `OncePerRequestFilter`, which guarantees a single execution per request (important because a request can be dispatched multiple times — async, error, forward). Implement `doFilterInternal(request, response, filterChain)` and always call `filterChain.doFilter(...)` unless you deliberately short-circuit. ### Interaction with ExceptionTranslationFilter If your custom filter can throw `AuthenticationException` or `AccessDeniedException` and you want Spring to translate it into a clean 401/403, place it **after** `ExceptionTranslationFilter`. If it runs before, you must handle those exceptions yourself or they surface as 500s. For authentication filters that *set* the entry point behavior, extending `AbstractAuthenticationProcessingFilter` wires the failure handling for you. ### Removing/replacing built-ins You don't add filters by re-declaring them; you toggle features via the DSL (e.g. `.formLogin(...)` adds `UsernamePasswordAuthenticationFilter`; `.csrf(csrf -> csrf.disable())` removes `CsrfFilter`). `addFilterAt` combined with disabling the built-in is the pattern for a true swap.

  • Why is addFilterAt risky compared to addFilterBefore/After?
    addFilterAt assigns your filter the same order value as the reference filter, and the relative order of two filters sharing a position is undefined. Use it only to replace a filter whose built-in you've removed; otherwise prefer Before/After for deterministic ordering.
  • You place a custom JWT filter with addFilterBefore(UsernamePasswordAuthenticationFilter.class) but it runs before ExceptionTranslationFilter. It throws AccessDeniedException and you get a 500 instead of 403. Why?
    ExceptionTranslationFilter only catches exceptions from filters downstream of it. A filter placed before it must handle its own AuthenticationException/AccessDeniedException; otherwise the exception escapes untranslated and becomes a 500.

saying these in an interview costs you the question

  • Claiming you pass an integer order to place a custom filter among built-ins.
  • Using addFilterAt and expecting a guaranteed order relative to the reference filter.
  • Passing a non-registered filter class as the reference and expecting it to work.
  • Forgetting to call filterChain.doFilter, silently swallowing the request.

context