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.
Detailed Explanation:
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.
DelegatingFilterProxy, forwards the request to the Spring bean named springSecurityFilterChain.FilterChainProxy, which looks through your configured SecurityFilterChain beans in order and picks the first one whose matcher fits the request.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.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.
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.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.
| Concept | Where it runs | Can block auth? | Best use |
|---|---|---|---|
| SecurityFilterChain | Servlet filter layer | Yes | Login, auth, CSRF |
| HandlerInterceptor | Spring MVC layer | Usually no | Logging, 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.
permitAll() does not skip the chain. The request still passes through security; it just skips authorization denial for that path.SessionCreationPolicy.STATELESS.securityMatcher("/api/**") on the API chain, that chain may apply too broadly and catch requests you meant for another chain.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.
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:
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.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.@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.SecurityContext, which is the per-request holder for the current Authentication.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.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:
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.@Order controls that order.Practice Tasks:
/public, /api/hello, and /admin with different users.securityMatcher("/api/**") and observe how the API behavior changes./actuator/** and make it require only an admin role.