Spring Boot/SecurityFilterChain DSL — config Spring Security 6 lambda style
~24 phútSpring Security cơ bảnMiễn phí

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

RuleEffect
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

11. Tóm tắt

  • @Bean SecurityFilterChain replace WebSecurityConfigurerAdapter (Spring Security 6).
  • Lambda DSL mandatory: http.csrf(csrf -> csrf.disable()).
  • requestMatchers() replace antMatchers(). 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.STATELESS cho REST API + JWT.
  • CSRF disable cho stateless REST API.
  • Custom exceptionHandling trả Problem Details JSON cho REST API.
  • WebSecurityCustomizer bypass filter cho static asset.
  • @EnableMethodSecurity enable @PreAuthorize/@PostAuthorize.
  • Production pattern: stateless + CSRF disable + CORS config + JWT resource server + custom error handler + security headers.

12. Tự kiểm tra

Tự kiểm tra
Q1
Spring 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 + reuse

Workflow migration Spring Security 5 → 6:

  1. Identify chained .and() blocks.
  2. Replace với .method(lambda) structure.
  3. Remove extends WebSecurityConfigurerAdapter.
  4. Convert to @Bean SecurityFilterChain.
  5. 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()
);

2 vấn đề:

  1. `.anyRequest()` đứng trước specific rules: first match wins. Mọi request match `.anyRequest()` first, never reach `/api/admin/**` rule.
  2. 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 all

Fix — order specific → general:

http.authorizeHttpRequests(auth -> auth
  .requestMatchers("/api/public/**").permitAll()
  .requestMatchers("/api/admin/**").hasRole("ADMIN")
  .anyRequest().authenticated()                 // Catch-all LAST
);

Generalized rule:

  1. Most specific rule first.
  2. Public endpoints (permitAll) early.
  3. Role-based protection middle.
  4. 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.

Q3
App 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):

  1. Different auth method per area:
    • Admin: client certificate (mTLS) auth.
    • API: JWT Bearer.
    • Web: form login + cookie session.
  2. Different JWT issuer:
    • Admin: internal JWT (different signing key).
    • Public: external IdP (Auth0, Cognito).
  3. Different filter:
    • Admin: extra audit filter, IP whitelist.
    • Public: rate limit filter.
  4. 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.
Q4
SessionCreationPolicy có 4 option. Khi nào pick cái nào?
PolicyBehaviorUse case
STATELESSSpring không create / use HTTP session. Không read existing session.REST API + JWT — auth từ Bearer token mỗi request.
NEVERKhô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.
ALWAYSAlways 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 needed

JWT 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 after
Q5
Custom `authenticationEntryPoint` + `accessDeniedHandler` cho REST API. Vì sao default Spring Security không phù hợp?

Default Spring Security behavior:

  • 401 Unauthorized: redirect to /login page (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+json consistent.
Q6
Production 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, metrics

Khi 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...