Backend engineer path · A request's journey · Topic 8 of 18

The Spring Security filter chain

Not started

Spring Security plugs into your application as a single servlet filter, but that filter contains a whole chain of its own. The container calls a DelegatingFilterProxy, which forwards to the FilterChainProxy bean; that picks the first SecurityFilterChain whose request matcher fits the URL and runs its filters in a fixed order: headers, CSRF, logout, the authentication filters, exception translation and finally authorisation. Only after the last one says yes does the request reach the DispatcherServlet.

Why it matters: Security problems in Spring apps are usually ordering problems; once you can read the filter list, a 401 or a missing CSRF check stops being a mystery.

  1. 1One filter, many inside

    DelegatingFilterProxy hands requests to FilterChainProxy, which runs the filters of the first SecurityFilterChain that matches.

  2. 2Authenticate, then authorise

    Authentication filters establish who the caller is; AuthorizationFilter, near the end, decides whether they may proceed.

  3. 3The context is per request

    The result lands in the SecurityContextHolder, which code later in the request can read.

  4. 4Configure with beans

    In Spring Security 6 you declare SecurityFilterChain beans with the lambda DSL; WebSecurityConfigurerAdapter is gone.

1

How it works

The explanation, with examples you can run.

A typical API configuration

@Configuration@EnableMethodSecurityclass SecurityConfig {   @Bean  @Order(1)  SecurityFilterChain api(HttpSecurity http) throws Exception {    return http        .securityMatcher("/api/**")        .authorizeHttpRequests(auth -> auth            .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()            .requestMatchers("/api/admin/**").hasRole("ADMIN")            .anyRequest().authenticated())        .oauth2ResourceServer(oauth -> oauth.jwt(Customizer.withDefaults()))        .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))        .csrf(csrf -> csrf.disable())   // stateless API using bearer tokens, not cookies        .build();  }   @Bean  @Order(2)  SecurityFilterChain web(HttpSecurity http) throws Exception {    return http        .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())        .formLogin(Customizer.withDefaults())        .build();  }}

Two chains, two policies: the API validates JWT bearer tokens without sessions, the rest of the site uses a login form with sessions and CSRF protection on. Disable CSRF only where the browser does not send credentials automatically; cookie-based sessions need it.

What the filters do

  • SecurityContextHolderFilter loads any existing security context (for example from the session) for this request.
  • Authentication filters (BearerTokenAuthenticationFilter, UsernamePasswordAuthenticationFilter, BasicAuthenticationFilter) turn credentials into an Authentication via the AuthenticationManager and store it in the SecurityContextHolder.
  • AnonymousAuthenticationFilter sets an anonymous identity if nothing else did.
  • ExceptionTranslationFilter catches security exceptions thrown further down and turns them into a 401 (start authentication) or a 403 (access denied).
  • AuthorizationFilter checks the authorizeHttpRequests rules, first match wins.

Start-up logs the exact list for each chain at DEBUG level for org.springframework.security, which is the quickest way to see what is really configured.

After the filters

@EnableMethodSecurity adds a second line of defence on beans, using the same proxies as @Transactional:

@PreAuthorize("hasRole('ADMIN') or #customerId == authentication.name")public Invoice invoice(String customerId) { ... }

And because the context is held per request, a controller can simply ask for it:

@GetMapping("/me")Profile me(@AuthenticationPrincipal Jwt jwt) { return profiles.find(jwt.getSubject()); }
2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.An app defines two SecurityFilterChain beans, one for /api/** and one for any request. A request for /api/orders arrives. Which chains run?
  2. 2.An unauthenticated request reaches a protected URL. Which filter turns the AccessDeniedException into a 401 or a redirect to the login page?
  3. 3.You add spring-boot-starter-security and nothing else. What happens?

0 of 3 right so far. Get all three to finish this topic.