Spring Boot/Spring Security architecture — Filter chain, AuthenticationManager, SecurityContext
~26 phútSpring Security cơ bảnMiễn phí

Spring Security architecture — Filter chain, AuthenticationManager, SecurityContext

Spring Security là servlet filter chain với 15+ filter. Bài này bóc Filter Chain Proxy, DelegatingFilterProxy, SecurityFilterChain, AuthenticationManager flow, SecurityContextHolder ThreadLocal, Authentication object, GrantedAuthority, AccessDecisionManager. Hiểu architecture trước syntax.

Module 04 đã build TaskFlow v2 — REST API + Postgres + JPA. Mọi endpoint public — anyone access được. Module 05 này thêm Spring Security: authentication (ai bạn?) + authorization (bạn được phép gì?).

Trước khi viết code, bài đầu tiên bóc architecture của Spring Security. Đây là module phức tạp nhất Spring — 15+ filter, ThreadLocal context, voter pattern, multiple auth provider. Hiểu architecture rồi, syntax DSL bài 02 sẽ hiện ra logic.

1. Spring Security là gì

Spring Security = framework cho 2 vấn đề:

  • Authentication (xác thực): chứng minh "tôi là user X" — username/password, OAuth2 token, certificate.
  • Authorization (phân quyền): "user X có được làm action Y không" — role, permission, custom rule.

Implement qua servlet filter chain intercept mọi HTTP request trước khi đến DispatcherServlet (Module 03 bài 01).

flowchart LR
    Req[HTTP Request]
    Tomcat[Tomcat]
    Filters["Spring Security<br/>FilterChainProxy<br/>(15+ filters)"]
    DS[DispatcherServlet]
    Ctrl["@RestController"]

    Req --> Tomcat --> Filters --> DS --> Ctrl
    Filters -->|"reject"| Resp401[401/403 response]

Filter reject = controller never receive request. Security boundary external to business code.

2. Servlet Filter — foundation

Servlet API định nghĩa Filter interface:

public interface Filter {
    void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
        throws IOException, ServletException;
}

Filter chạy theo chain — mỗi filter call chain.doFilter() để pass next:

public class TimingFilter implements Filter {
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
        long start = System.currentTimeMillis();
        chain.doFilter(req, res);              // pass to next filter
        log.info("Took {}ms", System.currentTimeMillis() - start);
    }
}

Spring Security build trên cơ chế này — provide chain của ~15 filter.

3. FilterChainProxy — gateway

Thay vì register 15 filter riêng vào servlet container, Spring Security register 1 filter wrapper (FilterChainProxy) tại Tomcat. Wrapper delegate đến chain Spring-managed:

flowchart TB
    Tomcat[Tomcat]
    DFP["DelegatingFilterProxy<br/>(servlet container level)"]
    FCP["FilterChainProxy<br/>(Spring bean)"]
    SFC["SecurityFilterChain<br/>(Spring bean — your config)"]
    F1[CsrfFilter]
    F2[CorsFilter]
    F3[BearerTokenAuthFilter]
    F4[BasicAuthFilter]
    F5[AuthorizationFilter]
    Etc[...]

    Tomcat --> DFP --> FCP --> SFC
    SFC --> F1 --> F2 --> F3 --> F4 --> F5 --> Etc

3 layer:

LayerClassRole
Servlet containerDelegatingFilterProxyBridge từ Tomcat sang Spring bean
Spring infraFilterChainProxyMatch URL pattern → chọn SecurityFilterChain
Spring bean (your config)SecurityFilterChainList of filter cho specific URL pattern

DelegatingFilterProxy lý do tồn tại: Tomcat startup filter trước Spring context. Proxy lazy-fetch Spring bean tại request đầu tiên.

4. 15+ filters của Spring Security

Default filter chain Spring Security 6 (đơn giản hoá):

flowchart TB
    F1[DisableEncodeUrlFilter]
    F2[ForceEagerSessionCreationFilter]
    F3[ChannelProcessingFilter]
    F4[WebAsyncManagerIntegrationFilter]
    F5[SecurityContextHolderFilter]
    F6[HeaderWriterFilter]
    F7[CorsFilter]
    F8[CsrfFilter]
    F9[LogoutFilter]
    F10[OAuth2AuthorizationRequestRedirectFilter]
    F11[Saml2WebSsoAuthenticationFilter]
    F12[UsernamePasswordAuthenticationFilter]
    F13[BearerTokenAuthenticationFilter]
    F14[BasicAuthenticationFilter]
    F15[RequestCacheAwareFilter]
    F16[SecurityContextHolderAwareRequestFilter]
    F17[AnonymousAuthenticationFilter]
    F18[ExceptionTranslationFilter]
    F19[AuthorizationFilter]

    F1 --> F2 --> F3 --> F4 --> F5 --> F6 --> F7 --> F8 --> F9 --> F10 --> F11 --> F12 --> F13 --> F14 --> F15 --> F16 --> F17 --> F18 --> F19

Top 5 filter quan trọng nhất bạn sẽ chạm:

FilterRole
SecurityContextHolderFilterLoad SecurityContext từ session/repo vào ThreadLocal
CorsFilterHandle CORS preflight + headers
CsrfFilterCSRF token validation (form-based)
UsernamePasswordAuthenticationFilterForm login (POST /login username/password)
BearerTokenAuthenticationFilterJWT/OAuth2 token (Authorization: Bearer ...)
BasicAuthenticationFilterHTTP Basic auth
AuthorizationFilterCuối chain — check authority match endpoint rules
ExceptionTranslationFilterCatch AuthenticationException/AccessDeniedException → 401/403

Mỗi filter có 1 trách nhiệm. Filter order matter — SecurityContextHolderFilter phải chạy trước mọi filter khác.

5. Authentication flow

sequenceDiagram
    participant Req as HTTP Request
    participant Filter as Auth Filter<br/>(BearerTokenAuthenticationFilter)
    participant AM as AuthenticationManager
    participant AP as AuthenticationProvider<br/>(JwtAuthenticationProvider)
    participant Holder as SecurityContextHolder
    participant Ctrl as Controller

    Req->>Filter: Authorization Bearer eyJ...
    Filter->>Filter: Extract token from header
    Filter->>AM: authenticate(BearerTokenAuthenticationToken)
    AM->>AP: try authenticate
    AP->>AP: Validate JWT signature, expiry
    AP-->>AM: Authentication (authenticated)
    AM-->>Filter: Authentication
    Filter->>Holder: SecurityContextHolder.getContext().setAuthentication()
    Filter->>Ctrl: chain.doFilter() — pass
    Ctrl->>Holder: SecurityContextHolder.getContext().getAuthentication()
    Holder-->>Ctrl: Authentication object
    Ctrl->>Req: response 200

Key concepts:

5.1 Authentication — credential + result

public interface Authentication {
    String getName();                            // username/principal name
    Object getPrincipal();                       // user object (UserDetails, JWT subject)
    Object getCredentials();                     // password/token (clear after auth)
    Collection<? extends GrantedAuthority> getAuthorities();    // roles/permissions
    boolean isAuthenticated();
    Object getDetails();                         // request metadata (IP, session)
}

Authentication đại diện 2 state:

  • Trước auth: credential request (username + password). isAuthenticated() = false.
  • Sau auth: principal + authorities. isAuthenticated() = true. Credential cleared (security).

5.2 AuthenticationManager — entry point

public interface AuthenticationManager {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
}

Single method. Filter call manager.authenticate(unauthenticated) → trả authenticated hoặc throw exception.

Default impl: ProviderManager — delegate đến list AuthenticationProvider. Try từng provider until 1 success hoặc all fail.

5.3 AuthenticationProvider — pluggable strategy

public interface AuthenticationProvider {
    Authentication authenticate(Authentication authentication) throws AuthenticationException;
    boolean supports(Class<?> authentication);
}

Different provider cho different auth method:

ProviderToken type
DaoAuthenticationProviderUsernamePasswordAuthenticationToken (form login)
JwtAuthenticationProviderBearerTokenAuthenticationToken
Saml2AuthenticationProviderSaml2AuthenticationToken
CustomCustom

ProviderManager iterate, ask supports(), call authenticate() if supports.

5.4 UserDetailsService — user lookup

DaoAuthenticationProvider use UserDetailsService to lookup user from DB:

public interface UserDetailsService {
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}

public interface UserDetails {
    String getUsername();
    String getPassword();                        // hashed
    Collection<? extends GrantedAuthority> getAuthorities();
    boolean isAccountNonExpired();
    boolean isAccountNonLocked();
    boolean isCredentialsNonExpired();
    boolean isEnabled();
}

App custom: implement UserDetailsService query DB:

@Service
public class AppUserDetailsService implements UserDetailsService {

    private final UserRepository userRepo;

    public UserDetails loadUserByUsername(String email) {
        User user = userRepo.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));

        return new org.springframework.security.core.userdetails.User(
            user.getEmail(),
            user.getPassword(),                  // BCrypt hash
            user.getRoles().stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).toList()
        );
    }
}

Module 05 bài 03 đào sâu.

6. SecurityContext + ThreadLocal

SecurityContextHolder static class quản context per-thread:

public class SecurityContextHolder {
    private static SecurityContextHolderStrategy strategy;

    public static SecurityContext getContext();
    public static void setContext(SecurityContext context);
    public static void clearContext();
}

Default strategy: ThreadLocalSecurityContextHolderStrategy — store per-thread.

Pattern usage:

@RestController
public class OrderController {

    @GetMapping("/me")
    public String me() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        return "Hello " + auth.getName();
    }
}

Hoặc cleaner — @AuthenticationPrincipal:

@GetMapping("/me")
public String me(@AuthenticationPrincipal UserDetails user) {
    return "Hello " + user.getUsername();
}

Pitfall: @Async/thread pool không propagate ThreadLocal. Module 05 bài 04 đào sâu.

7. Authorization — AccessDecision

After auth, AuthorizationFilter check user có quyền access endpoint:

http.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .requestMatchers("/api/projects/**").hasAnyRole("USER", "ADMIN")
    .requestMatchers("/api/public/**").permitAll()
    .anyRequest().authenticated()
);

AuthorizationFilter invoke AuthorizationManager:

public interface AuthorizationManager<T> {
    AuthorizationDecision check(Supplier<Authentication> authentication, T object);
}

AuthorizationDecision:

  • granted = true → allow.
  • granted = false → throw AccessDeniedException.

ExceptionTranslationFilter catch AccessDeniedException → 403 Forbidden.

7.1 GrantedAuthority

Authentication.getAuthorities() return list of GrantedAuthority. Default impl: SimpleGrantedAuthority(String authority).

Convention 2 type:

  • Role: "ROLE_USER", "ROLE_ADMIN" — coarse-grained.
  • Permission: "orders:read", "orders:write" — fine-grained.

hasRole("ADMIN") shortcut cho hasAuthority("ROLE_ADMIN") — Spring auto-prefix.

8. Exception flow

flowchart TB
    Req[Request]
    AuthFilter[Authentication Filter]
    AuthzFilter[Authorization Filter]
    Ctrl[Controller]
    Resp[Response]

    Req --> AuthFilter
    AuthFilter -->|"valid"| AuthzFilter
    AuthFilter -->|"throw AuthenticationException<br/>(invalid token)"| 401[401 Unauthorized]
    AuthzFilter -->|"granted"| Ctrl
    AuthzFilter -->|"throw AccessDeniedException<br/>(no permission)"| 403[403 Forbidden]
    Ctrl --> Resp

    style 401 fill:#fee
    style 403 fill:#fee

ExceptionTranslationFilter map:

  • AuthenticationException → 401 Unauthorized (chưa login đúng).
  • AccessDeniedException → 403 Forbidden (login OK nhưng không quyền).

Anonymous user (chưa login) gặp protected endpoint → AuthenticationException → 401 + redirect to login (form-based) hoặc empty body (REST API).

9. Spring Security 6 changes vs 5

Big rewrite từ Spring Security 5 → 6 (Boot 3.0+):

AspectSpring Security 5Spring Security 6
ConfigurationWebSecurityConfigurerAdapter (extend)SecurityFilterChain bean
DSL stylehttp.authorizeRequests()http.authorizeHttpRequests()
Lambda DSLOptionalMandatory (mostly)
antMatchers()AvailableRemoved — use requestMatchers()
Java baseline817
Spring Boot2.7-3.0+

Bài 02 đào sâu DSL Spring Security 6.

10. Boot autoconfig

Add dependency:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Boot autoconfig kích hoạt:

  • SecurityAutoConfiguration — register FilterChainProxy, default user (random password).
  • WebSecurityEnablerConfiguration@EnableWebSecurity implicit.
  • UserDetailsServiceAutoConfiguration — default UserDetailsService với 1 user user + random password log console.

Default behavior:

  • All endpoint protected — require auth.
  • Form login enabled at /login.
  • HTTP Basic enabled.
  • CSRF enabled.
  • Default user user + password generated random (log to console).

Production: override với custom config — bài 02.

11. Pitfall tổng hợp

Nhầm 1: Hardcode credential trong code. ✅ Use UserDetailsService query DB. BCrypt hash password. Module 05 bài 03.

Nhầm 2: Tin Spring Security tự handle CORS/CSRF. ✅ CORS/CSRF cần config explicit. Default behavior có thể block frontend. Module 05 bài 06.

Nhầm 3: Inject Authentication qua SecurityContextHolder everywhere. ✅ Use @AuthenticationPrincipal cho controller. Cleaner.

Nhầm 4: Test endpoint không setup auth — fail 401 unexpected. ✅ Use @WithMockUser hoặc @WithUserDetails cho test slice.

Nhầm 5: Confuse hasRole("ADMIN") vs hasAuthority("ADMIN"). ✅ hasRole("ADMIN") = hasAuthority("ROLE_ADMIN") (auto-prefix). Use 1 convention nhất quán.

Nhầm 6: Custom WebSecurityConfigurerAdapter extend (Spring Security 5). ✅ Spring Security 6 removed. Use SecurityFilterChain bean.

Nhầm 7: SecurityContextHolder cross thread (@Async, executor pool). ✅ Setup DelegatingSecurityContextExecutor hoặc pass auth qua method param.

12. 📚 Deep Dive Spring Reference

📚 Tài liệu chính chủ

Spring Security:

Source code:

Tutorials:

Tool:

  • /actuator/mappings — show secured endpoints.
  • IntelliJ "Spring Security" tool window.

13. Tóm tắt

  • Spring Security giải quyết authentication + authorization qua servlet filter chain.
  • 3 layer: Tomcat → DelegatingFilterProxyFilterChainProxySecurityFilterChain (your config).
  • ~15 filter trong default chain. Top 5: SecurityContextHolderFilter, CorsFilter, CsrfFilter, UsernamePasswordAuthenticationFilter, AuthorizationFilter.
  • Authentication object = credential + principal + authorities. State trước/sau auth.
  • AuthenticationManager entry point — delegate đến list AuthenticationProvider. Each provider handle different token type.
  • UserDetailsService lookup user từ DB cho DaoAuthenticationProvider.
  • SecurityContextHolder ThreadLocal store auth per-thread. @AuthenticationPrincipal clean access trong controller.
  • AuthorizationFilter check via AuthorizationManager — return granted/denied. AccessDecision pattern.
  • GrantedAuthority: 2 convention — "ROLE_X" (coarse) hoặc "resource:action" (fine-grained).
  • Exception: AuthenticationException → 401, AccessDeniedException → 403. ExceptionTranslationFilter map.
  • Spring Security 6: WebSecurityConfigurerAdapter removed → SecurityFilterChain bean. antMatchers() removed → requestMatchers(). Java 17 baseline.
  • Boot autoconfig kích hoạt với spring-boot-starter-security. Default user + random password.

14. Tự kiểm tra

Tự kiểm tra
Q1
Spring Security register 1 filter ở Tomcat (`DelegatingFilterProxy`) thay vì 15 filter riêng. Vì sao?

3 lý do thiết kế:

  1. Spring lifecycle awareness: Tomcat startup filter trước Spring context. 15 filter Spring-managed không thể register direct vào Tomcat — chúng cần Spring bean (UserDetailsService, AuthenticationManager, ...). DelegatingFilterProxy lazy-load Spring bean tại request đầu — work cả khi Spring context chưa ready lúc Tomcat init.
  2. Dynamic filter chain: User config can define multiple SecurityFilterChain với different URL pattern. FilterChainProxy match URL → chọn chain đúng. 1 wrapper handle dynamic dispatch.
  3. Flexibility config: Add/remove filter trong Spring config without touching Tomcat. Servlet container chỉ thấy 1 filter — Spring quản internals.

Layer architecture:

Tomcat
└── DelegatingFilterProxy (registered as servlet filter)
  └── FilterChainProxy (Spring bean — looks up by URL)
      └── SecurityFilterChain (Spring bean — your config)
          └── 15 filters (CSRF, auth, authorization, ...)

Verify runtime:

# Show registered filters
GET /actuator/mappings
# DelegatingFilterProxy registered for "/*"

# Show security filter chain
GET /actuator/security
# (need Spring Security Actuator)

Pattern enterprise: nếu app có public + admin path với khác filter:

@Bean
@Order(1)
public SecurityFilterChain adminChain(HttpSecurity http) {
  http.securityMatcher("/api/admin/**")
      .authorizeHttpRequests(...)
      ...;
  return http.build();
}

@Bean
@Order(2)
public SecurityFilterChain publicChain(HttpSecurity http) {
  http.securityMatcher("/api/public/**")
      .authorizeHttpRequests(auth -> auth.anyRequest().permitAll())
      ...;
  return http.build();
}

Multiple chain với different rule. FilterChainProxy route theo URL.

Q2
Authentication object có 2 state: trước auth, sau auth. Field nào differ? Cho code ví dụ.

Trước auth (request input):

  • name: username (string).
  • principal: typically same as name.
  • credentials: password / token raw.
  • authorities: empty list.
  • isAuthenticated(): false.

Sau auth (provider result):

  • name: username.
  • principal: UserDetails object full.
  • credentials: null (cleared cho security — không leak password).
  • authorities: full list of granted authority (roles/permissions).
  • isAuthenticated(): true.

Code ví dụ flow:

// 1. Filter receive request
String username = request.getParameter("username");
String password = request.getParameter("password");

// 2. Build Authentication trước auth
Authentication unauthenticated = new UsernamePasswordAuthenticationToken(username, password);
// unauthenticated.isAuthenticated() = false
// unauthenticated.getCredentials() = "raw_password"
// unauthenticated.getAuthorities() = []

// 3. Pass to AuthenticationManager
Authentication authenticated = authManager.authenticate(unauthenticated);

// 4. Provider validate → return new Authentication
// authenticated.isAuthenticated() = true
// authenticated.getCredentials() = null (CLEARED)
// authenticated.getAuthorities() = [ROLE_USER, ROLE_ADMIN]
// authenticated.getPrincipal() = UserDetails object

// 5. Store in context
SecurityContextHolder.getContext().setAuthentication(authenticated);

Vì sao clear credentials sau auth:

  • Memory leak risk: password sit in heap → memory dump expose.
  • Logging risk: accidentally log Authentication object → password in log.
  • Defense in depth: sau auth, password không cần nữa — clear immediately.

Set credential erasure:

@Bean
public AuthenticationManager authManager(...) {
  ProviderManager pm = new ProviderManager(...);
  pm.setEraseCredentialsAfterAuthentication(true);   // default true
  return pm;
}
Q3
So sánh `hasRole("ADMIN")` vs `hasAuthority("ROLE_ADMIN")` vs `hasAuthority("ADMIN")`. Khi nào pick cái nào?
MethodCheck authorityConvention
hasRole("ADMIN")"ROLE_ADMIN" (auto-prefix)Role-based
hasAuthority("ROLE_ADMIN")"ROLE_ADMIN" exactRole-based explicit
hasAuthority("ADMIN")"ADMIN" exactPermission-based

Verify equivalent:

// User has authority "ROLE_ADMIN"
hasRole("ADMIN")               → TRUE  (auto-prefix → check "ROLE_ADMIN")
hasAuthority("ROLE_ADMIN")     → TRUE  (exact match)
hasAuthority("ADMIN")          → FALSE (looking for "ADMIN", user has "ROLE_ADMIN")

// User has authority "ADMIN" (no prefix)
hasRole("ADMIN")               → FALSE (looking for "ROLE_ADMIN", user has "ADMIN")
hasAuthority("ROLE_ADMIN")     → FALSE
hasAuthority("ADMIN")          → TRUE  (exact match)

2 convention:

Convention 1 — Role-based (coarse):

Roles: ROLE_USER, ROLE_ADMIN, ROLE_MANAGER

@PreAuthorize("hasRole('ADMIN')")
@PreAuthorize("hasAnyRole('USER', 'MANAGER')")

http.authorizeHttpRequests(auth -> auth
  .requestMatchers("/admin/**").hasRole("ADMIN")
);

Pros: simple, intuitive. Cons: coarse-grained — role "ADMIN" có quyền gì? Implicit.

Convention 2 — Permission-based (fine-grained):

Authorities: orders:read, orders:write, orders:delete, projects:read, ...

@PreAuthorize("hasAuthority('orders:write')")
@PreAuthorize("hasAnyAuthority('orders:read', 'projects:read')")

http.authorizeHttpRequests(auth -> auth
  .requestMatchers(HttpMethod.GET, "/orders/**").hasAuthority("orders:read")
  .requestMatchers(HttpMethod.POST, "/orders/**").hasAuthority("orders:write")
);

Pros: explicit, flexible. Cons: more authorities to manage.

Convention 3 — Hybrid (industry pattern):

User → Roles → Permissions

User Alice has roles: [USER, MANAGER]
Role USER has permissions: [orders:read, projects:read]
Role MANAGER has permissions: [orders:write, projects:write, users:read]

Granted authorities expanded: [
ROLE_USER, ROLE_MANAGER,                       // roles
orders:read, orders:write, projects:read, projects:write, users:read    // permissions
]

@PreAuthorize("hasRole('MANAGER')")              // role-based check
@PreAuthorize("hasAuthority('orders:write')")    // permission-based check

Pros: best of both. UI navigate by role, fine-grained check by permission.

Recommend cho TaskFlow:

  • Start với role-based (simple): ROLE_USER, ROLE_ADMIN.
  • Migrate sang permission-based khi role explosion (10+ role).
  • Hybrid khi enterprise scale — Module 11 đào sâu.
Q4
Đoạn code sau truy cập SecurityContext. Có 2 vấn đề. Liệt kê + fix.
@RestController
public class OrderController {

  @GetMapping("/me")
  public String me() {
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      return "Hello " + auth.getName();
  }

  @PostMapping("/orders")
  @Async
  public CompletableFuture<Order> placeAsync(...) {
      return CompletableFuture.supplyAsync(() -> {
          String currentUser = SecurityContextHolder.getContext().getAuthentication().getName();
          return orderService.placeOrder(currentUser, ...);
      });
  }
}
  1. Direct SecurityContextHolder access trong controller — verbose, không type-safe:
    @GetMapping("/me")
    public String me() {
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      return "Hello " + auth.getName();
    }

    Fix — @AuthenticationPrincipal:

    @GetMapping("/me")
    public String me(@AuthenticationPrincipal UserDetails user) {
      return "Hello " + user.getUsername();
    }
    
    // Or with custom user type
    @GetMapping("/me")
    public String me(@AuthenticationPrincipal AppUser user) {
      return "Hello " + user.getName();
    }
    Pros: cleaner, type-safe (compile error nếu cast wrong), test easier (mock UserDetails arg).
  2. SecurityContextHolder cross thread — ThreadLocal not propagated to @Async thread:
    @Async
    public CompletableFuture<Order> placeAsync(...) {
      return CompletableFuture.supplyAsync(() -> {
          SecurityContextHolder.getContext().getAuthentication().getName();
          // CRASH — SecurityContext null trong async thread!
      });
    }

    Vì sao crash:

    • SecurityContextHolder default strategy: ThreadLocalSecurityContextHolderStrategy.
    • ThreadLocal not propagated khi switch thread (executor pool).
    • Async thread no SecurityContext → getAuthentication() returns null → NPE.

    3 cách fix:

    Cách 1 — Pass current user qua method param:

    @PostMapping("/orders")
    public CompletableFuture<Order> placeAsync(
      @AuthenticationPrincipal UserDetails user,
      @RequestBody OrderRequest req
    ) {
      String username = user.getUsername();        // capture trên main thread
      return CompletableFuture.supplyAsync(() -> {
          return orderService.placeOrder(username, req);    // pass param
      });
    }

    Cách 2 — DelegatingSecurityContextExecutor:

    @Configuration
    public class AsyncConfig implements AsyncConfigurer {
    
      @Override
      public Executor getAsyncExecutor() {
          ThreadPoolTaskExecutor delegate = new ThreadPoolTaskExecutor();
          delegate.initialize();
          return new DelegatingSecurityContextExecutor(delegate);
          // Wrapper auto-propagate SecurityContext to async thread
      }
    }

    Cách 3 — InheritableThreadLocal strategy:

    spring.security.strategy=MODE_INHERITABLETHREADLOCAL

    Child thread inherit context. Risky cho thread pool reuse — leak context giữa request.

    Recommend: Cách 1 (pass param) — simple, explicit, no magic.

Q5
Default Spring Boot Security autoconfig: tất cả endpoint protected, default user "user" + random password. Workflow dev local + production khác nhau ra sao?

Dev local (default Boot):

Add dependency: spring-boot-starter-security
Run: mvn spring-boot:run

Console output:
"Using generated security password: 8a7b6c5d-..."

Workflow:
1. Browser GET /api/projects → 401
2. Redirect to /login form
3. Username: "user", Password: "8a7b6c5d-..."
4. Login → access granted

Default config:

  • Single user "user" (in-memory).
  • Random password generated lúc startup, log to console.
  • Form login enabled at /login.
  • Basic auth enabled.
  • CSRF enabled.
  • All endpoint require auth.

Vì sao default này: security-by-default. Dev không thể accidentally expose unprotected endpoint. Force opt-in cho public endpoint.

Production setup:

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      http
          .csrf(csrf -> csrf.disable())                    // REST API — disable CSRF
          .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
          .authorizeHttpRequests(auth -> auth
              .requestMatchers("/api/auth/**").permitAll()
              .requestMatchers("/actuator/health").permitAll()
              .requestMatchers("/api/admin/**").hasRole("ADMIN")
              .anyRequest().authenticated()
          )
          .oauth2ResourceServer(oauth2 -> oauth2.jwt(...))    // JWT auth
          .exceptionHandling(eh -> eh
              .authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
              .accessDeniedHandler(new BearerTokenAccessDeniedHandler())
          );

      return http.build();
  }

  @Bean
  public PasswordEncoder passwordEncoder() {
      return new BCryptPasswordEncoder(12);    // strength 12
  }

  @Bean
  public UserDetailsService userDetailsService(UserRepository userRepo) {
      return username -> userRepo.findByEmail(username)
          .map(AppUserDetails::new)
          .orElseThrow(() -> new UsernameNotFoundException(username));
  }
}

Production config differences:

  • CSRF disabled: stateless API không cần CSRF token.
  • Stateless session: không create HTTP session — pure JWT.
  • JWT resource server: validate Bearer token thay form login.
  • Database UserDetailsService: query DB users + roles.
  • BCrypt password encoder: hash production passwords.
  • Custom auth entry point: 401 JSON response thay redirect to /login.
  • Permit specific public endpoints: /api/auth/login, /actuator/health.

Workflow dev:

  1. Setup User entity (Module 04 đã có).
  2. Implement UserDetailsService.
  3. Add BCryptPasswordEncoder.
  4. Override SecurityFilterChain.
  5. Add JWT issue + validate (Module 05 bài 04).
  6. Test: register → login → JWT → access protected endpoint.

Module 05 bài 02-07 build full pipeline.

Q6
Spring Security 6 removed `WebSecurityConfigurerAdapter`. Migration pattern từ Spring Security 5 sang 6 ra sao?

Spring Security 5 (deprecated):

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
      http.authorizeRequests()
          .antMatchers("/admin/**").hasRole("ADMIN")
          .antMatchers("/public/**").permitAll()
          .anyRequest().authenticated()
          .and()
          .formLogin()
          .and()
          .csrf().disable();
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
      auth.userDetailsService(userDetailsService)
          .passwordEncoder(passwordEncoder());
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
      return super.authenticationManagerBean();
  }
}

Spring Security 6 (current):

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
      http
          .authorizeHttpRequests(auth -> auth                  // (1) Renamed
              .requestMatchers("/admin/**").hasRole("ADMIN")    // (2) Renamed antMatchers
              .requestMatchers("/public/**").permitAll()
              .anyRequest().authenticated()
          )
          .formLogin(Customizer.withDefaults())                 // (3) Lambda DSL mandatory
          .csrf(csrf -> csrf.disable());

      return http.build();                                       // (4) Build SecurityFilterChain bean
  }

  @Bean
  public AuthenticationManager authManager(
      UserDetailsService uds,
      PasswordEncoder encoder
  ) {
      DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
      provider.setUserDetailsService(uds);
      provider.setPasswordEncoder(encoder);
      return new ProviderManager(provider);
  }
}

4 changes:

  1. WebSecurityConfigurerAdapter removed — not extend, return SecurityFilterChain bean.
  2. antMatchers() removed — use requestMatchers(). Smarter — auto-detect AntPath vs MVC pattern.
  3. authorizeRequests() renamed authorizeHttpRequests() (more powerful).
  4. Lambda DSL mandatory — chain .and() deprecated, use lambda http.csrf(csrf -> csrf.disable()).

Migration steps:

  1. Bump dependency: Spring Boot 3.0+ (auto-pull Spring Security 6).
  2. Replace extends WebSecurityConfigurerAdapter@Bean SecurityFilterChain.
  3. Replace antMatchersrequestMatchers.
  4. Replace authorizeRequestsauthorizeHttpRequests.
  5. Replace chained .and() → lambda DSL.
  6. Replace configure(AuthenticationManagerBuilder)@Bean AuthenticationManager với manual provider setup.
  7. Test: ensure all rule still apply (Postman + verify endpoint protection).

Tooling:

# Auto-migration tool
mvn org.openrewrite.maven:rewrite-maven-plugin:run \
  -Drewrite.activeRecipes=org.openrewrite.java.spring.security6.UpgradeSpringSecurity_6_0

OpenRewrite recipe automate phần lớn transformation.

Khoá này: Spring Security 6 syntax từ đầu. Module 05 bài 02 đào sâu DSL.

Bài tiếp theo: SecurityFilterChain DSL — config Spring Security 6 lambda style

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