Handbooks / Spring Boot / Chapter 7
Spring Security
7 pages · ~4 min✓ Reviewed
Builds on Database Migrations. Next up: Caching, Async and Scheduling.
Filter Chain and Auth Model
1. The Filter Chain
- DelegatingFilterProxyservlet filter
- FilterChainProxySpring Security entry
- SecurityFilterChainfirst match wins
- 1SecurityContextHolderFilter
- 2CorsFilter
- 3CsrfFilter
- 4Authentication filterUsernamePasswordAuthenticationFilter or custom JWT filter
- 5AuthorizationFilteralways last
2. Authentication vs Authorization
| Authentication | Authorization | |
|---|---|---|
| Asks | Who are you? | What may you do? |
| Failure | 401 Unauthorized | 403 Forbidden |
| When | missing or invalid credentials | authenticated but denied |
3. Authentication Flow
- Filterbuilds Authentication token
- ProviderManagerAuthenticationManager
- AuthenticationProviderUserDetailsService + PasswordEncoder
- SecurityContextHolderstores the result
SecurityContextHolder is thread-local. Values do not reach new threads or async tasks unless a delegating strategy is configured.
SecurityFilterChain Configuration
1. The Chain Bean
- Declare a @Bean SecurityFilterChain taking
HttpSecurity - End the method with
http.build() - WebSecurityConfigurerAdapter is removed in Spring Security 6
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated());
return http.build();
}Lambda DSL
2. Matching Rules
- Rules match top to bottom, first match wins
- Put specific matchers before broader ones
- Finish with
anyRequest()
Password Encoding
1. Why Hash, and With What
- Never store plain text or fast hashes (MD5, SHA-1)
- Use an adaptive salted hash via
PasswordEncoder - Two methods:
encode(raw)andmatches(raw, stored)
| Encoder | Notes |
|---|---|
BCryptPasswordEncoder | Standard bean; strength 10; salt inside the hash |
Argon2PasswordEncoder | Memory-hard alternative |
SCryptPasswordEncoder | Memory-hard alternative |
2. Delegating Encoder
JWT Stateless Authentication
1. JWT Anatomy
- header.payload.signature, each part Base64URL
- Signed, not encrypted: anyone can read the payload
- Claims:
sub,exp,iat,roles - Never put secrets in the payload
2. Login and Filter Flow
- Stateless:
STATELESSsession policy, CSRF disabled for token-only APIs - Login calls
AuthenticationManager.authenticate(UsernamePasswordAuthenticationToken) - Returns a signed token with a short
exp
- Loginauthenticate()
- Signed JWTshort exp
- Bearer headerevery request
- JWT filterOncePerRequestFilter
- SecurityContextHolderAuthentication set
Method Security, CORS and CSRF
1. Method Security
@EnableMethodSecurityreplaces@EnableGlobalMethodSecurity@PreAuthorizechecks before the method;@PostAuthorizechecks after it- SpEL sees arguments and principal: owner-only access
- Proxy-based: self-invocation is not checked
@PreAuthorize("hasRole('ADMIN')") void purge() { } @PreAuthorize("#id == authentication.principal.id") User get(Long id) { }
second rule: owner-only access
2. CORS vs CSRF
| CORS | CSRF | |
|---|---|---|
| Is | browser rule, response headers | token on POST/PUT/DELETE |
Security Cheat Sheet
1. Status Codes and Config
| Check | Means |
|---|---|
| 401 | not authenticated |
| 403 | authenticated but forbidden |
hasRole('X') | authority ROLE_X |
hasAuthority('X') | the exact string |
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(a -> a
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated());
return http.build();
}order rules specific to general
2. Passwords and JWT
Check yourself
Rules are /api/** authenticated, then /api/public/** permitAll. An anonymous user calls /api/public/ping. Result?
- Blocked: login required.
- First match wins, so the broad rule catches it.
- Put the specific matcher first.
Login always fails. Why? encoder.encode(raw).equals(stored)
- Each encode adds a fresh salt, so the strings differ.
- Use
encoder.matches(raw, stored).
A browser shows a CORS error on POST. The network tab shows the OPTIONS preflight got 401. Fix?
- Security rejected the credential-less preflight.
- Enable
http.cors(withDefaults())with aCorsConfigurationSourcebean.