SecurityFilterChain DSL — config Spring Security 6 lambda style
Spring Security 6 dùng SecurityFilterChain bean + lambda DSL. Bài này bóc HttpSecurity API, requestMatchers, authorizeHttpRequests, multiple SecurityFilterChain với @Order, securityMatcher, ignoring static resources, exceptionHandling, sessionManagement, headers.
Bài 01 bóc architecture. Bài này bóc DSL config — cú pháp lambda Spring Security 6 cho HttpSecurity. Sau bài này, bạn viết được security config production-grade từ scratch.
1. SecurityFilterChain bean — replace WebSecurityConfigurerAdapter
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.exceptionHandling(eh -> eh
.authenticationEntryPoint((req, res, ex) -> {
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.setContentType("application/problem+json");
res.getWriter().write("""
{"type":"about:blank","title":"Unauthorized","status":401}
""");
})
);
return http.build();
}
}
@Bean SecurityFilterChain = config style 2026. Pattern uniform: chain .method(lambda) calls.
2. requestMatchers() — URL pattern
Replace antMatchers() từ Spring Security 5. Smarter — auto-detect MVC vs servlet:
http.authorizeHttpRequests(auth -> auth
// String pattern (Ant-style)
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/orders/{id}/cancel").hasRole("USER")
// HTTP method specific
.requestMatchers(HttpMethod.GET, "/api/projects/**").hasAnyRole("USER", "ADMIN")
.requestMatchers(HttpMethod.POST, "/api/projects").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/projects/**").hasRole("ADMIN")
// Multiple patterns at once
.requestMatchers("/api/auth/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
// Regex pattern (less common)
.requestMatchers(RegexRequestMatcher.regexMatcher("/api/v[12]/.*")).authenticated()
// MVC matcher (with PathPatternParser)
.requestMatchers(MvcRequestMatcher).authenticated()
// Catch-all default
.anyRequest().authenticated()
);
Order matter — first match wins. Catch-all .anyRequest() luôn cuối.
3. Authorization rules — built-in
| Rule | Effect |
|---|---|
permitAll() | Anyone (kể cả anonymous) |
denyAll() | Reject everyone |
authenticated() | Any authenticated user |
anonymous() | Anonymous only (rare) |
fullyAuthenticated() | Authenticated, không remember-me |
hasRole("ADMIN") | Has authority "ROLE_ADMIN" |
hasAnyRole("USER", "ADMIN") | Has any of |
hasAuthority("orders:write") | Exact authority |
hasAnyAuthority("a", "b") | Has any of |
access(authorizationManager) | Custom logic |
access() cho complex rule:
.requestMatchers("/api/orders/{orderId}").access(
AuthorizationManagers.allOf(
AuthorityAuthorizationManager.hasRole("USER"),
new OrderOwnerAuthorizationManager(orderRepo)
)
)
OrderOwnerAuthorizationManager custom logic check user is order owner.
4. Multiple SecurityFilterChain
App có public API + admin API với rule khác → 2 chain:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain adminChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/admin/**") // SCOPE — chain only for /api/admin
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.anyRequest().hasRole("ADMIN")
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain publicChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/public/**")
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
);
return http.build();
}
@Bean
@Order(3)
public SecurityFilterChain defaultChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}
}
securityMatcher() define scope — chain chỉ apply cho URL match. @Order thứ tự — first match wins.
Use case:
- Different auth method per area (admin → JWT strict, public → none).
- Different filter chain per concern (REST API stateless, Web app stateful).
- Microservice with internal vs external endpoints.
5. Common HttpSecurity DSL methods
5.1 CSRF
http.csrf(csrf -> csrf.disable());
// Hoac tinh chinh
http.csrf(csrf -> csrf
.ignoringRequestMatchers("/api/webhooks/**")
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
);
REST API stateless → disable CSRF. Web app form-based → enable. Module 05 bài 06 chi tiết.
5.2 CORS
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://olhub.org"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
Module 05 bài 06 đào sâu.
5.3 Session management
http.sessionManagement(s -> s
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // REST API
// .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // default
// .sessionCreationPolicy(SessionCreationPolicy.ALWAYS) // session always
// .sessionCreationPolicy(SessionCreationPolicy.NEVER) // no create
.maximumSessions(1) // 1 session per user
.maxSessionsPreventsLogin(false) // kick old session
);
STATELESS cho REST API + JWT — không create HTTP session.
5.4 Form login
http.formLogin(form -> form
.loginPage("/login") // custom login page
.loginProcessingUrl("/api/auth/login") // POST endpoint
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error")
.usernameParameter("email")
.passwordParameter("password")
.permitAll()
);
// Hoac default
http.formLogin(Customizer.withDefaults()); // /login UI default
Server-side rendered web app. REST API thường skip — dùng JWT.
5.5 HTTP Basic
http.httpBasic(Customizer.withDefaults());
Browser hiện popup login. Useful cho admin endpoint internal.
5.6 OAuth2 Resource Server (JWT)
http.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
JwtGrantedAuthoritiesConverter authConverter = new JwtGrantedAuthoritiesConverter();
authConverter.setAuthoritiesClaimName("roles");
authConverter.setAuthorityPrefix("ROLE_");
converter.setJwtGrantedAuthoritiesConverter(authConverter);
return converter;
}
Validate JWT token incoming. Module 05 bài 04 đào sâu.
5.7 Exception handling
http.exceptionHandling(eh -> eh
.authenticationEntryPoint((req, res, ex) -> {
// 401 — unauthenticated
res.setStatus(HttpStatus.UNAUTHORIZED.value());
res.setContentType("application/problem+json");
res.getWriter().write("""
{"type":"about:blank","title":"Unauthorized","status":401,"detail":"Authentication required"}
""");
})
.accessDeniedHandler((req, res, ex) -> {
// 403 — authenticated but no permission
res.setStatus(HttpStatus.FORBIDDEN.value());
res.setContentType("application/problem+json");
res.getWriter().write("""
{"type":"about:blank","title":"Forbidden","status":403,"detail":"Insufficient permission"}
""");
})
);
Override default behavior — return JSON Problem Details thay redirect HTML.
5.8 Headers
http.headers(headers -> headers
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.sameOrigin())
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
.xssProtection(xss -> xss.disable()) // deprecated
.referrerPolicy(rp -> rp.policy(ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN))
);
Security headers production. Default Spring Security set common ones — override khi cần.
6. WebSecurityCustomizer — ignore endpoints
Static resources không cần security filter chain → bypass:
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/static/**", "/css/**", "/js/**", "/images/**", "/favicon.ico");
}
Pros:
- Bypass full filter chain — performance.
- Không log/track static asset.
Cons:
- No security check at all — careful không expose sensitive files.
Use case: CSS/JS/images. Không dùng cho /api/health — vẫn nên trong chain với permitAll() để log.
7. Component-level @EnableMethodSecurity
Bonus — method-level security:
@Configuration
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig { ... }
Enable @PreAuthorize, @PostAuthorize, @Secured. Bài 05 đào sâu.
8. Pattern thực tế — REST API stateless với JWT
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(
"/api/auth/**",
"/actuator/health",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-ui.html"
).permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(jwtAuthConverter()))
)
.exceptionHandling(eh -> eh
.authenticationEntryPoint(restAuthenticationEntryPoint())
.accessDeniedHandler(restAccessDeniedHandler())
)
.headers(h -> h
.frameOptions(f -> f.deny())
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://olhub.org"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
config.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
}
Pattern reference cho TaskFlow Module 05 bài 07.
9. Pitfall tổng hợp
❌ Nhầm 1: extends WebSecurityConfigurerAdapter.
✅ Spring Security 6 removed. Use @Bean SecurityFilterChain.
❌ Nhầm 2: Order rules wrong — catch-all trước specific.
.anyRequest().authenticated()
.requestMatchers("/admin/**").hasRole("ADMIN") // Never reached!
✅ Specific rules trước, catch-all cuối.
❌ Nhầm 3: Forgot csrf().disable() cho REST API.
✅ Stateless API → disable CSRF (no session, JWT-based).
❌ Nhầm 4: Multiple chain không securityMatcher().
@Bean @Order(1) public SecurityFilterChain a(HttpSecurity h) { /* no securityMatcher */ }
@Bean @Order(2) public SecurityFilterChain b(HttpSecurity h) { /* */ }
// Chain 1 match all requests, chain 2 never reached
✅ Each chain (except last) needs .securityMatcher(pattern).
❌ Nhầm 5: Forgot return http.build().
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) {
http.authorizeHttpRequests(...);
return null; // BUG — no chain registered
}
✅ Always return http.build().
❌ Nhầm 6: permitAll() cho /actuator/** blanket.
✅ Specific endpoint: /actuator/health permit. /actuator/loggers, /actuator/env need auth.
❌ Nhầm 7: Custom auth entry point return HTML cho REST API. ✅ JSON Problem Details. Frontend SPA expect JSON.
10. 📚 Deep Dive Spring Reference
Spring Security:
- Spring Security — HttpSecurity
- Authorize HttpRequests
- Multiple SecurityFilterChain
- Session Management
Migration:
Best practice:
11. Tóm tắt
@Bean SecurityFilterChainreplaceWebSecurityConfigurerAdapter(Spring Security 6).- Lambda DSL mandatory:
http.csrf(csrf -> csrf.disable()). requestMatchers()replaceantMatchers(). Smarter, auto-detect MVC.- Authorization rules order matter — specific trước, catch-all cuối.
- Multiple chain với
@Order+securityMatcher()cho different scope (admin vs public). SessionCreationPolicy.STATELESScho REST API + JWT.- CSRF disable cho stateless REST API.
- Custom exceptionHandling trả Problem Details JSON cho REST API.
WebSecurityCustomizerbypass filter cho static asset.@EnableMethodSecurityenable@PreAuthorize/@PostAuthorize.- Production pattern: stateless + CSRF disable + CORS config + JWT resource server + custom error handler + security headers.
12. Tự kiểm tra
Q1Spring Security 6 lambda DSL — vì sao mandatory? Lợi ích vs chained `.and()` style cũ?▸
Old style (deprecated Spring Security 5):
http
.csrf().disable()
.and()
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.and()
.formLogin()
.loginPage("/login")
.and()
.httpBasic();Vấn đề chained `.and()`:
- Confusing scope: hard to tell which method belongs to which group.
.and()exit current group? - IDE indentation: hard to format consistently.
- Chain break easily: miss
.and()→ compile error obscure. - Less type-safe: some method valid in 1 context but not another — runtime error possible.
Lambda DSL (Spring Security 6 mandatory):
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
)
.httpBasic(Customizer.withDefaults());Lợi ích:
- Clear scope: each lambda block = self-contained config for that feature.
- Compile-time safe: lambda parameter type-checked.
- Easier IDE format: nested lambda align natural.
- Default config easy:
.formLogin(Customizer.withDefaults())apply default settings without nesting. - Functional style: consistent với modern Java/Spring.
Customizer pattern:
// Inline lambda
http.csrf(csrf -> csrf.disable());
// Default
http.httpBasic(Customizer.withDefaults());
// Reusable Customizer bean
@Component
public class StrictCsrfCustomizer implements Customizer<CsrfConfigurer<HttpSecurity>> {
public void customize(CsrfConfigurer<HttpSecurity> csrf) {
csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
csrf.ignoringRequestMatchers("/api/webhooks/**");
}
}
http.csrf(strictCsrfCustomizer); // inject + reuseWorkflow migration Spring Security 5 → 6:
- Identify chained
.and()blocks. - Replace với
.method(lambda)structure. - Remove
extends WebSecurityConfigurerAdapter. - Convert to
@Bean SecurityFilterChain. - Tool: OpenRewrite recipe automate phần lớn.
Q2Đoạn config sau có 2 vấn đề. Endpoint nào bị broken?http.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/public/**").permitAll()
);
▸
http.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/public/**").permitAll()
);2 vấn đề:
- `.anyRequest()` đứng trước specific rules: first match wins. Mọi request match `.anyRequest()` first, never reach `/api/admin/**` rule.
- Result: `/api/admin/**` cần auth (any user) — không enforce ADMIN role. `/api/public/**` cần auth — không public!
Test:
# Login as USER role
POST /api/auth/login → JWT
# Try admin endpoint
GET /api/admin/users
Authorization: Bearer eyJ... # USER token
→ 200 OK (BUG! Should be 403)
# Match anyRequest().authenticated() first — USER is authenticated → granted
# Try public endpoint without auth
GET /api/public/info
→ 401 Unauthorized (BUG! Should be 200)
# anyRequest().authenticated() catch allFix — order specific → general:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated() // Catch-all LAST
);Generalized rule:
- Most specific rule first.
- Public endpoints (permitAll) early.
- Role-based protection middle.
- Catch-all
anyRequest()always last.
Test pattern verify:
@Test
void publicEndpoint_noAuth_returns200() throws Exception {
mockMvc.perform(get("/api/public/info"))
.andExpect(status().isOk());
}
@Test
@WithMockUser(roles = "USER")
void adminEndpoint_userRole_returns403() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isForbidden());
}
@Test
@WithMockUser(roles = "ADMIN")
void adminEndpoint_adminRole_returns200() throws Exception {
mockMvc.perform(get("/api/admin/users"))
.andExpect(status().isOk());
}Test 3 case verify config — catch order bug rapidly.
Q3App có 2 SecurityFilterChain: admin (`/api/admin/**`) + default. Khi nào nên 1 chain duy nhất, khi nào multiple?▸
Single chain (recommend default):
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/public/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(...));
return http.build();
}Pros: 1 config place, easy understand, reduce coupling.
Multiple chain (specialized scenario):
@Bean @Order(1)
public SecurityFilterChain adminChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/admin/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth.anyRequest().hasRole("ADMIN"))
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.decoder(adminJwtDecoder())) // separate JWK source
)
.addFilterBefore(new AdminAuditFilter(), AuthorizationFilter.class);
return http.build();
}
@Bean @Order(2)
public SecurityFilterChain publicChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/v1/**")
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}Khi nào multiple chain (use case):
- Different auth method per area:
- Admin: client certificate (mTLS) auth.
- API: JWT Bearer.
- Web: form login + cookie session.
- Different JWT issuer:
- Admin: internal JWT (different signing key).
- Public: external IdP (Auth0, Cognito).
- Different filter:
- Admin: extra audit filter, IP whitelist.
- Public: rate limit filter.
- Stateless vs stateful:
- Web app: session-based.
- REST API: JWT stateless.
Quy tắc:
- Default: single chain với
requestMatchers()rules. - Multiple chain khi có real need: different auth method, filter, session policy.
- Don't multi-chain "phòng hờ" — adds complexity without benefit.
Q4SessionCreationPolicy có 4 option. Khi nào pick cái nào?▸
| Policy | Behavior | Use case |
|---|---|---|
STATELESS | Spring không create / use HTTP session. Không read existing session. | REST API + JWT — auth từ Bearer token mỗi request. |
NEVER | Không create new session, nhưng **dùng** existing nếu có. | Mixed REST + legacy session — phasing out session. |
IF_REQUIRED (default) | Create session khi cần (form login). Không tự create unnecessarily. | Server-side rendered web app standard. |
ALWAYS | Always create session. | Legacy app yêu cầu session for tracking. |
Recommend cho TaskFlow:
http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));REST API stateless — JWT-based auth. No HTTP session needed.
Pros stateless:
- Scale horizontally: no session affinity needed. Any pod handle any request.
- No session storage: không cần Redis cho session replication.
- Self-contained auth: JWT token chứa user info — no DB lookup mỗi request.
- Cleaner API: client → pure HTTP, no cookie magic.
Cons stateless:
- Logout khó: token valid cho duration, không revoke easily. Need denylist (Redis blacklist token id).
- Token bigger than session id: JWT 500-2000 bytes vs session ID 32 bytes. Bandwidth concern.
- Token leak risk: JWT contains payload — leak = expose user info.
STATELESS pitfall:
// SAI — SecurityContextRepository default = HttpSessionSecurityContextRepository
// STATELESS không tự config repository → SecurityContext không persist
http.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
// Phải pair với:
.securityContext(ctx -> ctx.requireExplicitSave(true)) // explicit save
// Or for JWT — context built mỗi request từ token, no save neededJWT pattern (auth từ token mỗi request):
http
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2.jwt(...));
// BearerTokenAuthenticationFilter parse JWT mỗi request
// SecurityContext built per-request, garbage collected afterQ5Custom `authenticationEntryPoint` + `accessDeniedHandler` cho REST API. Vì sao default Spring Security không phù hợp?▸
Default Spring Security behavior:
- 401 Unauthorized: redirect to
/loginpage (HTML form). - 403 Forbidden: render error page.
Vấn đề cho REST API:
- Client là SPA frontend (React, Vue) — expect JSON, không HTML redirect.
- Mobile app expect status code + error JSON.
- Postman/curl gặp redirect HTML không meaningful.
- Frontend không thể parse HTML error → cannot show user-friendly message.
Custom REST entry point + handler:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.exceptionHandling(eh -> eh
.authenticationEntryPoint(restAuthEntryPoint())
.accessDeniedHandler(restAccessDeniedHandler())
)
// ... other config
return http.build();
}
@Bean
public AuthenticationEntryPoint restAuthEntryPoint() {
return (request, response, authException) -> {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
Map<String, Object> body = Map.of(
"type", "https://api.olhub.org/errors/unauthorized",
"title", "Unauthorized",
"status", 401,
"detail", authException.getMessage(),
"instance", request.getRequestURI()
);
new ObjectMapper().writeValue(response.getOutputStream(), body);
};
}
@Bean
public AccessDeniedHandler restAccessDeniedHandler() {
return (request, response, accessDeniedException) -> {
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType(MediaType.APPLICATION_PROBLEM_JSON_VALUE);
Map<String, Object> body = Map.of(
"type", "https://api.olhub.org/errors/forbidden",
"title", "Forbidden",
"status", 403,
"detail", "Insufficient permission to access this resource",
"instance", request.getRequestURI()
);
new ObjectMapper().writeValue(response.getOutputStream(), body);
};
}
}Response client receives:
HTTP/1.1 401 Unauthorized
Content-Type: application/problem+json
{
"type": "https://api.olhub.org/errors/unauthorized",
"title": "Unauthorized",
"status": 401,
"detail": "Full authentication is required",
"instance": "/api/projects/42"
}
HTTP/1.1 403 Forbidden
Content-Type: application/problem+json
{
"type": "https://api.olhub.org/errors/forbidden",
"title": "Forbidden",
"status": 403,
"detail": "Insufficient permission",
"instance": "/api/admin/users"
}Frontend SPA parse JSON, show user-friendly toast:
fetch("/api/projects").then(res => {
if (res.status === 401) {
// Redirect to login page
window.location = "/login";
} else if (res.status === 403) {
// Show toast "You don't have permission"
toast.error("Permission denied");
}
});Pattern unified với Module 03 Problem Details:
- Web layer (controller exception):
@RestControllerAdvice+ProblemDetail. - Security layer (filter exception): custom
authenticationEntryPoint+accessDeniedHandler. - Both return
application/problem+jsonconsistent.
Q6Production config có cần `WebSecurityCustomizer.ignoring()` cho `/actuator/health`? Tradeoff?▸
2 cách xử lý `/actuator/health`:
Cách 1 — `WebSecurityCustomizer.ignoring()` (bypass filter chain):
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> web.ignoring()
.requestMatchers("/actuator/health");
}Pros:
- Performance: bypass full filter chain — no auth check, no SecurityContext setup, no logging.
- Independent of security state: health check work even if SecurityContext has issue.
- K8s liveness probe: simple HTTP check, no token needed.
Cons:
- No security headers: response missing CSP, HSTS — but health endpoint trivial response.
- No logging through Security audit: request not in security audit log.
Cách 2 — `permitAll()` trong filter chain:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
// ... other rules
);Pros:
- Endpoint goes through full filter chain — get security headers, audit log.
- Consistent với other endpoints — easier mental model.
Cons:
- Slightly slower (filter chain overhead).
Recommend cho TaskFlow:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/info").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN") // other actuator endpoints
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);Use permitAll() trong chain. Selective expose actuator endpoint:
/actuator/health,/actuator/info: public (K8s probe + version info)./actuator/loggers,/actuator/env,/actuator/conditions: ADMIN only.
Production critical: default Boot 3 expose only health, info. Other endpoint must explicit:
management:
endpoints:
web:
exposure:
include: health, info, conditions, loggers, mappings, metricsKhi nào dùng `WebSecurityCustomizer.ignoring()`:
- Static resources: CSS/JS/images — performance + no security needed.
- Health check probe path nếu gặp bottleneck performance.
- Public assets:
/favicon.ico,/robots.txt.
Anti-pattern:
// SAI — bypass /actuator/loggers
@Bean
public WebSecurityCustomizer wsc() {
return web -> web.ignoring().requestMatchers("/actuator/**");
}
// Anyone can change log level + see env (with env endpoint)Always selective. Specific endpoint, not blanket.
Bài tiếp theo: Form login & Basic auth — UserDetailsService, BCrypt, in-memory + DB
Bài này có giúp bạn hiểu bản chất không?
Bình luận (0)
Đang tải...