Skip to content

Spring Boot · Security

Spring Security Basics: The Filter Chain, Explained

5 min read Updated 2026-06-26 Share:

Practice Security Basics interview questions

Adding one dependency locks down everything

The first surprising thing about Spring Security is how aggressive it is. Add the starter and every endpoint is suddenly behind a login with a password printed to your console:

Using generated security password: 3f9a1c8e-...-b21

Security is opt-out, not opt-in. There's no half-secured state where you forgot to protect one route — the default is "deny everything, prove who you are." You then relax and shape the rules deliberately. That philosophy explains a lot of the framework's behavior, and it's the right starting point for an interview answer: Spring Security secures first and lets you open up from there.

It's filters all the way down

Under the hood, Spring Security is a single servlet Filter that delegates to an ordered chain of smaller filters. A request runs a gauntlet — CSRF, authentication, authorization — before it ever reaches your controller:

request → CSRF filter → authentication filter → ExceptionTranslationFilter
        → AuthorizationFilter → DispatcherServlet → your @Controller

Each filter has one job. If authentication fails, a filter short-circuits the chain and writes a 401; if authorization fails, you get a 403 — both before any controller code runs. Once you internalize "it's filters before MVC," most of Spring Security's behavior stops being mysterious. A rejected request was stopped by a specific filter, not by your code.

Authentication vs authorization

These two words get used loosely, but Spring keeps them strictly separate. Authentication establishes identity — verifying you are who you claim. Authorization decides permissions — whether that identity may do a thing:

http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/admin/**").hasRole("ADMIN")   // authorization
        .anyRequest().authenticated())                   // authentication
    .httpBasic(withDefaults());

Authentication always comes first. A 401 means "we don't know you — log in"; a 403 means "we know you, but you're not allowed." They're two different HTTP statuses precisely because they're two different questions.

Configuring with a SecurityFilterChain bean

Modern Spring Security (5.7+, Boot 2.7+) removed the old WebSecurityConfigurerAdapter. You now define a SecurityFilterChain bean: take the HttpSecurity builder, configure it, return build():

@Configuration
@EnableWebSecurity
class SecurityConfig {
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/public/**").permitAll()
                .anyRequest().authenticated())
            .formLogin(withDefaults());
        return http.build();
    }
}

Because it's just a bean, you can register several chains — each scoped by a securityMatcher and ordered with @Order — so one app can run stateless JWT for /api/** and form login for an admin UI side by side. Spring uses the first chain whose matcher matches.

The SecurityContext: where the current user lives

Once authenticated, the principal lives in a SecurityContext held by the SecurityContextHolder, which by default uses a ThreadLocal:

Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();

The thread-local detail matters: the current user is not automatically visible inside an @Async method or a new thread. In controllers, don't reach into the holder — inject @AuthenticationPrincipal or an Authentication parameter and let Spring resolve the user for you.

Never store a plaintext password

Spring Security forces the issue: it requires a PasswordEncoder, and storing a raw password throws There is no PasswordEncoder mapped for the id "null". The recommended default is BCrypt — salted, adaptive, deliberately slow:

@Bean
PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}
// signup:  encoder.encode(rawPassword)
// login:   encoder.matches(rawPassword, storedHash)

A DelegatingPasswordEncoder prefixes each hash with its algorithm ({bcrypt}…) so you can migrate algorithms over time. Fast hashes like MD5 or SHA-256 are wrong here — password hashing is supposed to be slow.

CSRF: keep it for cookies, drop it for tokens

CSRF protection stops a logged-in user's browser from being tricked into firing a state-changing request using its session cookie. Spring enables it by default. The nuance interviewers probe: when is it safe to disable?

// Stateless API authenticated by a bearer token — CSRF doesn't apply:
http.csrf(csrf -> csrf.disable())
    .sessionManagement(sm -> sm.sessionCreationPolicy(STATELESS));

CSRF only matters when the browser automatically attaches credentials — i.e. a cookie. A token API where the client must explicitly set an Authorization header isn't vulnerable, so disabling CSRF there is correct. For a cookie-session web app, keep it on — disabling it just to silence a 403 opens a real hole.

CORS is a relaxation, not a guard

CORS controls which browser origins may call your API. It's easy to confuse with security, but it grants access rather than restricting it — it relaxes the browser's same-origin block. Spring Security must be told to honor it, or the chain rejects the preflight before your CORS config runs:

http.cors(withDefaults());   // delegates to a CorsConfigurationSource bean

CORS never authenticates anyone. It only decides which origins the browser will let talk to you.

Stateless vs session

Finally, the model choice. Session-based security stores the context in the HTTP session (a JSESSIONID cookie) — simple, supports instant server-side logout, ideal for server-rendered apps. Stateless security keeps no session; every request carries its own credential (usually a JWT), so any node can serve any request:

http.sessionManagement(sm ->
    sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS));

Stateless scales out cleanly and suits SPAs and mobile clients; session-based is simpler and makes logout trivial. REST APIs lean stateless; server-rendered apps lean session.

Recap

Spring Security secures everything by default through an ordered filter chain that runs before MVC. Configure it with a SecurityFilterChain bean; separate authentication (401) from authorization (403); read the current user via @AuthenticationPrincipal from a thread-local SecurityContext; always hash passwords with BCrypt; keep CSRF for cookie apps and disable it only for stateless token APIs; treat CORS as a browser relaxation, not a guard; and pick stateless tokens for scale-out APIs, sessions for server-rendered apps. Get the filter-chain mental model right and the rest of Spring Security stops feeling like magic.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel