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.