RadhaPublication
HomeInterviewLearning PathsRole TracksMockBooksPricing
Login / Sign UpLog in

© 2026 Radha Publication. All rights reserved.

Shop on AmazonDonateContactPrivacyTerms
Spring Boot questions
HardSpring Boot#506 min readJul 11, 2026

How does SecurityFilterChain work?

Spring Boot
Spring Security
Servlet Filters
Authorization
Authentication
Practice modeTest yourself instead of reading straight through

Hook: Think of Spring Security like a building with a line of bouncers: the request must pass the right line, in the right order, before it reaches your controller.

Question: How does SecurityFilterChain work?

Answer: A SecurityFilterChain is Spring Security’s ordered set of filters that checks every HTTP request before it reaches your code. Spring first chooses the first chain whose matcher fits the request, then runs its filters one by one for tasks like authentication, authorization, CSRF, and session handling. If a filter blocks the request, the controller never runs.

Interview-Ready Answer: I think of SecurityFilterChain as the security path every request walks through. Spring Security receives the request through one servlet filter, picks the first matching chain, and then executes filters in order to authenticate, authorize, and possibly reject the request before it reaches a controller. A key detail is that multiple chains are matched by order, so the first matching one wins, which is why API and browser traffic are often split into different chains.

🧠 Memory Map
Memory map — visual summary of this topic

Detailed Explanation:

What it is

A SecurityFilterChain is a logical route of servlet filters for HTTP requests. The real servlet filter is FilterChainProxy; it chooses the first matching chain and then executes its filters in order. A RequestMatcher is the rule that decides which requests belong to a chain.

How it works under the hood

  1. The browser or client sends an HTTP request to the servlet container.
  2. Spring’s outer servlet filter, DelegatingFilterProxy, forwards the request to the Spring bean named springSecurityFilterChain.
  3. That bean is a FilterChainProxy, which looks through your configured SecurityFilterChain beans in order and picks the first one whose matcher fits the request.
  4. The chosen chain runs its filters in sequence. Common filters may read the session, load the current user, process login credentials, enforce CSRF, or check authorization rules.
  5. If a filter authenticates the user, it stores the result in the SecurityContext (the object that holds the current authentication). If a filter rejects the request, it writes the response immediately, often as 401 Unauthorized or 403 Forbidden.
  6. If nothing blocks the request, control finally reaches your controller or endpoint method.

That is why order matters so much. A filter can short-circuit the chain, which means it can stop everything that comes after it. For example, an authentication filter can challenge the client before any authorization rule even runs.

Why multiple chains help

One chain can secure a small app, but multiple chains are cleaner when browser traffic and API traffic need different rules. A common pattern is:

  • /api/** uses stateless auth such as HTTP Basic or JWT.
  • HTML pages use form login and sessions.
  • Admin endpoints get stricter roles.

Lower @Order values are checked first, so the first matching chain wins. If two chains could match the same request, the earlier one controls the outcome.

ConceptWhere it runsCan block auth?Best use
SecurityFilterChainServlet filter layerYesLogin, auth, CSRF
HandlerInterceptorSpring MVC layerUsually noLogging, request checks

The big mental model: use filters for security because they run before the controller and before MVC mapping finishes. Interceptors are useful, but they are not the right place for core authentication.

Performance and edge cases

  • Chain selection is O(n) in the number of chains. In real apps, that is usually 1 to 3 chains, so the cost is tiny.
  • Running the selected chain is O(f) in the number of filters in that chain. A typical setup has about a dozen filters, and the exact list changes with form login, basic auth, OAuth2, logout, and CSRF.
  • permitAll() does not skip the chain. The request still passes through security; it just skips authorization denial for that path.
  • CSRF is enabled by default for browser-style apps. For stateless APIs, you often disable it and set SessionCreationPolicy.STATELESS.
  • If you forget securityMatcher("/api/**") on the API chain, that chain may apply too broadly and catch requests you meant for another chain.
  • Most real bugs are not “security is broken”; they are “the wrong chain matched first,” which turns a 401 into a redirect or a 403 into a surprise success/failure.

Real-World Example: In a checkout service, the team had two audiences: mobile apps calling /api/orders/** and support staff using /admin/**. The API needed stateless authentication, while the admin UI needed form login and sessions. A developer forgot to narrow the API chain, so the browser-style chain started catching API requests first. In production, mobile clients began receiving 302 redirects to /login instead of JSON 401 responses, and payment retries spiked because the app treated HTML as a server error. The logs showed repeated saved-request messages, the controllers were never hit, and Kubernetes health checks became flaky when secured endpoints answered with redirects. Fixing the chain order and matchers restored the correct 401/403 behavior and stopped the incident.

Spring Boot
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
public class SecurityFilterChainDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecurityFilterChainDemoApplication.class, args);
    }

    @RestController
    static class DemoController {

        @GetMapping("/public")
        String publicPage() {
            return "Anyone can see this.";
        }

        @GetMapping("/api/hello")
        String apiHello(Authentication authentication) {
            // If you call this without Basic Auth, Spring Security stops the request
            // before this method runs and returns 401 Unauthorized.
            return "Hello, " + authentication.getName() + " from the API.";
        }

        @GetMapping("/admin")
        String admin(Authentication authentication) {
            // If a USER role calls /admin, the authorization filter returns 403 Forbidden.
            return "Admin area for " + authentication.getName();
        }
    }

    @Configuration
    static class SecurityConfig {

        @Bean
        @Order(1)
        SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
            http
                .securityMatcher("/api/**")
                // APIs are usually stateless; CSRF is mainly for browser sessions and forms.
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
                .httpBasic(Customizer.withDefaults());

            return http.build();
        }

        @Bean
        @Order(2)
        SecurityFilterChain appChain(HttpSecurity http) throws Exception {
            http
                // This chain handles everything that did NOT match /api/**.
                // If you forget securityMatcher on the API chain, the first chain can swallow all requests.
                .authorizeHttpRequests(auth -> auth
                    .requestMatchers("/public").permitAll()
                    .requestMatchers("/admin").hasRole("ADMIN")
                    .anyRequest().authenticated()
                )
                .formLogin(Customizer.withDefaults());

            return http.build();
        }

        @Bean
        UserDetailsService users() {
            return new InMemoryUserDetailsManager(
                User.withUsername("user")
                    .password("{noop}password")
                    .roles("USER")
                    .build(),
                User.withUsername("admin")
                    .password("{noop}password")
                    .roles("USER", "ADMIN")
                    .build()
            );
        }

        @Bean
        PasswordEncoder passwordEncoder() {
            return PasswordEncoderFactories.createDelegatingPasswordEncoder();
        }
    }
}

Follow-up & Tricky Questions:

  • How is SecurityFilterChain different from FilterChainProxy? SecurityFilterChain is the configuration for one matched security path, while FilterChainProxy is the real servlet filter that chooses among chains and runs the selected one.
  • What does securityMatcher do? It limits a chain to certain requests, such as /api/**. Without it, a chain may apply too broadly and unexpectedly protect pages you did not mean to match.
  • Why does @Order matter when you define multiple chains? Spring checks chains in order, and the first match wins. A lower order value is tried first, so it decides which security rules apply.
  • Where does Spring store the authenticated user? In the SecurityContext, which is the per-request holder for the current Authentication.
  • Why do stateless APIs often disable CSRF? CSRF protects browser sessions that automatically send cookies. If your API does not use a browser session, CSRF is usually unnecessary noise.
  • Tricky: If I use permitAll(), does the request skip security completely? No. The request still flows through the security filters; it only bypasses the authorization block for that path.
  • Tricky: If two chains match, are both applied? No. Spring Security chooses the first matching chain and stops looking, so ordering is critical.
  • Tricky: Is SecurityFilterChain the same as the servlet filter chain? No. It is Spring Security’s internal chain definition; the servlet container still sees a single outer filter that delegates to Spring Security.

Common Mistakes:

  • Making one giant chain for everything. Correction: Split API and browser traffic when they need different auth styles, because otherwise redirects, sessions, and CSRF rules fight each other.
  • Forgetting chain order. Correction: The first matching chain wins, so put the most specific chain first and make the matcher explicit.
  • Confusing 401 and 403. Correction: 401 means the user is not authenticated yet; 403 means the user is authenticated but lacks permission.
  • Disabling CSRF globally just to make one endpoint work. Correction: Disable it only for stateless API chains; keep it for browser form login flows.

Memory Hook: Pick the line, check the ID, then open the door. Spring first picks the matching chain, then the filters check identity and permissions before any controller sees the request.

Cheat Sheet:

  • SecurityFilterChain = ordered security filters for HTTP requests.
  • FilterChainProxy = real servlet filter that chooses the chain.
  • First matching chain wins; @Order controls that order.
  • Filters run before controllers, so they can return 401/403 immediately.
  • Use separate chains for API vs browser auth styles.
  • Chain selection is cheap; the real cost is usually your database or token verification.

Practice Tasks:

  • Run the sample app and try /public, /api/hello, and /admin with different users.
  • Remove securityMatcher("/api/**") and observe how the API behavior changes.
  • Add a third chain for /actuator/** and make it require only an admin role.
Previous
Back to Questions←→to navigate
Next

Recommended Resources

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

Recommended Book

Cracking the Coding Interview

189 Programming Questions and Solutions

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.Authentication; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.factory.PasswordEncoderFactories; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication public class SecurityFilterChainDemoApplication { public static void main(String[] args) { SpringApplication.run(SecurityFilterChainDemoApplication.class, args); } @RestController static class DemoController { @GetMapping("/public") String publicPage() { return "Anyone can see this."; } @GetMapping("/api/hello") String apiHello(Authentication authentication) { // If you call this without Basic Auth, Spring Security stops the request // before this method runs and returns 401 Unauthorized. return "Hello, " + authentication.getName() + " from the API."; } @GetMapping("/admin") String admin(Authentication authentication) { // If a USER role calls /admin, the authorization filter returns 403 Forbidden. return "Admin area for " + authentication.getName(); } } @Configuration static class SecurityConfig { @Bean @Order(1) SecurityFilterChain apiChain(HttpSecurity http) throws Exception { http .securityMatcher("/api/**") // APIs are usually stateless; CSRF is mainly for browser sessions and forms. .csrf(csrf -> csrf.disable()) .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()); return http.build(); } @Bean @Order(2) SecurityFilterChain appChain(HttpSecurity http) throws Exception { http // This chain handles everything that did NOT match /api/**. // If you forget securityMatcher on the API chain, the first chain can swallow all requests. .authorizeHttpRequests(auth -> auth .requestMatchers("/public").permitAll() .requestMatchers("/admin").hasRole("ADMIN") .anyRequest().authenticated() ) .formLogin(Customizer.withDefaults()); return http.build(); } @Bean UserDetailsService users() { return new InMemoryUserDetailsManager( User.withUsername("user") .password("{noop}password") .roles("USER") .build(), User.withUsername("admin") .password("{noop}password") .roles("USER", "ADMIN") .build() ); } @Bean PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } } }