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 --> Etc3 layer:
| Layer | Class | Role |
|---|---|---|
| Servlet container | DelegatingFilterProxy | Bridge từ Tomcat sang Spring bean |
| Spring infra | FilterChainProxy | Match URL pattern → chọn SecurityFilterChain |
| Spring bean (your config) | SecurityFilterChain | List 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 --> F19Top 5 filter quan trọng nhất bạn sẽ chạm:
| Filter | Role |
|---|---|
SecurityContextHolderFilter | Load SecurityContext từ session/repo vào ThreadLocal |
CorsFilter | Handle CORS preflight + headers |
CsrfFilter | CSRF token validation (form-based) |
UsernamePasswordAuthenticationFilter | Form login (POST /login username/password) |
BearerTokenAuthenticationFilter | JWT/OAuth2 token (Authorization: Bearer ...) |
BasicAuthenticationFilter | HTTP Basic auth |
AuthorizationFilter | Cuối chain — check authority match endpoint rules |
ExceptionTranslationFilter | Catch 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 200Key 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:
| Provider | Token type |
|---|---|
DaoAuthenticationProvider | UsernamePasswordAuthenticationToken (form login) |
JwtAuthenticationProvider | BearerTokenAuthenticationToken |
Saml2AuthenticationProvider | Saml2AuthenticationToken |
| Custom | Custom |
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→ throwAccessDeniedException.
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:#feeExceptionTranslationFilter 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+):
| Aspect | Spring Security 5 | Spring Security 6 |
|---|---|---|
| Configuration | WebSecurityConfigurerAdapter (extend) | SecurityFilterChain bean |
| DSL style | http.authorizeRequests() | http.authorizeHttpRequests() |
| Lambda DSL | Optional | Mandatory (mostly) |
antMatchers() | Available | Removed — use requestMatchers() |
| Java baseline | 8 | 17 |
| Spring Boot | 2.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— registerFilterChainProxy, default user (random password).WebSecurityEnablerConfiguration—@EnableWebSecurityimplicit.UserDetailsServiceAutoConfiguration— defaultUserDetailsServicevới 1 useruser+ 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
Spring Security:
- Spring Security Reference — full guide.
- Spring Security — Architecture — filter chain detail.
- Spring Security — Authentication
- Spring Security — Authorization
- Spring Security 6 Migration Guide — from 5.x to 6.x.
Source code:
Tutorials:
- Baeldung — Security Architecture
- Spring Security — In-Depth (book by Laur Spilcă) — 2024 edition for Spring Security 6.
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 →
DelegatingFilterProxy→FilterChainProxy→SecurityFilterChain(your config). - ~15 filter trong default chain. Top 5:
SecurityContextHolderFilter,CorsFilter,CsrfFilter,UsernamePasswordAuthenticationFilter,AuthorizationFilter. Authenticationobject = credential + principal + authorities. State trước/sau auth.AuthenticationManagerentry point — delegate đến listAuthenticationProvider. Each provider handle different token type.UserDetailsServicelookup user từ DB choDaoAuthenticationProvider.SecurityContextHolderThreadLocal store auth per-thread.@AuthenticationPrincipalclean access trong controller.AuthorizationFiltercheck viaAuthorizationManager— return granted/denied. AccessDecision pattern.GrantedAuthority: 2 convention —"ROLE_X"(coarse) hoặc"resource:action"(fine-grained).- Exception:
AuthenticationException→ 401,AccessDeniedException→ 403.ExceptionTranslationFiltermap. - Spring Security 6:
WebSecurityConfigurerAdapterremoved →SecurityFilterChainbean.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
Q1Spring Security register 1 filter ở Tomcat (`DelegatingFilterProxy`) thay vì 15 filter riêng. Vì sao?▸
3 lý do thiết kế:
- 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, ...).
DelegatingFilterProxylazy-load Spring bean tại request đầu — work cả khi Spring context chưa ready lúc Tomcat init. - Dynamic filter chain: User config can define multiple
SecurityFilterChainvới different URL pattern.FilterChainProxymatch URL → chọn chain đúng. 1 wrapper handle dynamic dispatch. - 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.
Q2Authentication 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:UserDetailsobject 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;
}Q3So sánh `hasRole("ADMIN")` vs `hasAuthority("ROLE_ADMIN")` vs `hasAuthority("ADMIN")`. Khi nào pick cái nào?▸
| Method | Check authority | Convention |
|---|---|---|
hasRole("ADMIN") | "ROLE_ADMIN" (auto-prefix) | Role-based |
hasAuthority("ROLE_ADMIN") | "ROLE_ADMIN" exact | Role-based explicit |
hasAuthority("ADMIN") | "ADMIN" exact | Permission-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 checkPros: 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, ...);
});
}
}
▸
@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, ...);
});
}
}- Direct
SecurityContextHolderaccess trong controller — verbose, không type-safe:@GetMapping("/me") public String me() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); return "Hello " + auth.getName(); }Fix —
@AuthenticationPrincipal:Pros: cleaner, type-safe (compile error nếu cast wrong), test easier (mock UserDetails arg).@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(); } SecurityContextHoldercross thread — ThreadLocal not propagated to@Asyncthread:@Async public CompletableFuture<Order> placeAsync(...) { return CompletableFuture.supplyAsync(() -> { SecurityContextHolder.getContext().getAuthentication().getName(); // CRASH — SecurityContext null trong async thread! }); }Vì sao crash:
SecurityContextHolderdefault 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_INHERITABLETHREADLOCALChild thread inherit context. Risky cho thread pool reuse — leak context giữa request.
Recommend: Cách 1 (pass param) — simple, explicit, no magic.
Q5Default 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 grantedDefault 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:
- Setup User entity (Module 04 đã có).
- Implement UserDetailsService.
- Add
BCryptPasswordEncoder. - Override SecurityFilterChain.
- Add JWT issue + validate (Module 05 bài 04).
- Test: register → login → JWT → access protected endpoint.
Module 05 bài 02-07 build full pipeline.
Q6Spring 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:
WebSecurityConfigurerAdapterremoved — not extend, returnSecurityFilterChainbean.antMatchers()removed — userequestMatchers(). Smarter — auto-detect AntPath vs MVC pattern.authorizeRequests()renamedauthorizeHttpRequests()(more powerful).- Lambda DSL mandatory — chain
.and()deprecated, use lambdahttp.csrf(csrf -> csrf.disable()).
Migration steps:
- Bump dependency: Spring Boot 3.0+ (auto-pull Spring Security 6).
- Replace
extends WebSecurityConfigurerAdapter→@Bean SecurityFilterChain. - Replace
antMatchers→requestMatchers. - Replace
authorizeRequests→authorizeHttpRequests. - Replace chained
.and()→ lambda DSL. - Replace
configure(AuthenticationManagerBuilder)→@Bean AuthenticationManagervới manual provider setup. - 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_0OpenRewrite 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...