Authentication flow & SecurityContext — từ request đến principal trong ThreadLocal
Bóc đúng một mảnh của Spring Security: request đi qua filter, AuthenticationManager điều phối AuthenticationProvider để trả Authentication hoặc ném exception; kết quả lưu vào SecurityContextHolder ThreadLocal per-request. Giải thích tại sao ThreadLocal, tại sao tách authentication/authorization, và cơ chế exception mapping 401/403.
TL;DR: Khi request mang Authorization: Bearer <token> vào Spring Security, BearerTokenAuthenticationFilter trích token, xây object Authentication chưa xác thực rồi giao cho AuthenticationManager.authenticate(). ProviderManager lần lượt hỏi từng AuthenticationProvider — provider nào khai báo supports() đúng loại token sẽ thực sự validate. Kết quả trả về là Authentication đã xác thực, được lưu vào SecurityContextHolder bằng ThreadLocal — mỗi thread (mỗi request) giữ context riêng, không lẫn. Cuối chain, AuthorizationFilter đọc context đó để quyết định cho qua hay ném AccessDeniedException. ExceptionTranslationFilter map: AuthenticationException → 401, AccessDeniedException → 403.
Bài 01 — Filter chain architecture đã bóc cấu trúc ba tầng DelegatingFilterProxy → FilterChainProxy → SecurityFilterChain. Bài này đào sâu vào cơ chế bên trong một lần xử lý request: authentication flow, SecurityContext, và exception path.
1. Scenario — request JWT vào API
TaskFlow v2 có endpoint GET /api/projects. Request từ frontend mang header:
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhbGljZUBleGFtcGxlLmNvbSIsInJvbGVzIjpbIlJPTEVfVVNFUiJdfQ.sig
Chuỗi câu hỏi cần trả lời:
- Filter nào trong chain chịu trách nhiệm đọc header đó?
- Token được validate ở đâu, bằng cơ chế gì?
- "Alice đã login" được lưu ở đâu để controller sau đó đọc được?
- Nếu token hết hạn, response 401 được sinh ra ở đâu trong chain?
Bốn câu hỏi đó chính xác là bốn thứ bài này giải thích.
2. Authentication flow — cơ chế bên dưới
Sơ đồ sequence đầy đủ của một request JWT qua Spring Security:
sequenceDiagram
participant Req as HTTP Request
participant BF as BearerTokenAuthFilter
participant PM as ProviderManager
participant AP as JwtAuthenticationProvider
participant Holder as SecurityContextHolder
participant AZ as AuthorizationFilter
participant Ctrl as Controller
Req->>BF: Authorization: Bearer eyJ...
BF->>BF: extract token from header
BF->>PM: authenticate(BearerTokenAuthenticationToken)
PM->>AP: supports(BearerTokenAuthenticationToken)?
AP-->>PM: true
PM->>AP: authenticate(token)
AP->>AP: validate signature + expiry + claims
AP-->>PM: JwtAuthenticationToken (authenticated)
PM-->>BF: JwtAuthenticationToken
BF->>Holder: setAuthentication(JwtAuthenticationToken)
BF->>AZ: chain.doFilter() -- pass
AZ->>Holder: getAuthentication()
Holder-->>AZ: JwtAuthenticationToken
AZ->>AZ: check hasRole / hasAuthority
AZ->>Ctrl: authorized -- pass
Ctrl->>Holder: getAuthentication() [optional]
Holder-->>Ctrl: JwtAuthenticationTokenBa actor quan trọng là AuthenticationManager, AuthenticationProvider, và SecurityContextHolder — mỗi actor có trách nhiệm tách biệt.
3. Authentication object — hai trạng thái
Authentication (xác thực) là interface trung tâm, đại diện danh tính của một request. Nó tồn tại ở hai trạng thái khác nhau trong một lần xử lý:
public interface Authentication {
String getName();
Object getPrincipal(); // user object (UserDetails, JWT subject)
Object getCredentials(); // password / token — cleared sau auth
Collection<? extends GrantedAuthority> getAuthorities();
boolean isAuthenticated();
Object getDetails(); // request metadata (IP, session id)
}
| Trạng thái | isAuthenticated() | getPrincipal() | getCredentials() | getAuthorities() |
|---|---|---|---|---|
| Trước xác thực (input) | false | username / raw token | password / token raw | rỗng |
| Sau xác thực (output) | true | UserDetails object | null (đã xoá) | roles / permissions đầy đủ |
Chuyển trạng thái chỉ xảy ra qua đúng một cửa — provider.authenticate():
flowchart LR
U["Token chua xac thuc<br/>principal = username<br/>credentials = raw password<br/>authorities = rong"]
P["provider.authenticate()"]
A["Token da xac thuc<br/>principal = UserDetails<br/>credentials = null (erased)<br/>authorities = ROLE_USER..."]
E["throw<br/>AuthenticationException"]
U --> P
P -->|"hop le"| A
P -->|"sai"| ELý do credentials bị xoá sau khi xác thực thành công: giảm rủi ro password hoặc raw token nằm trong heap quá lâu. Nếu memory dump bị lấy, token không còn ở đó. ProviderManager mặc định bật eraseCredentialsAfterAuthentication = true.
4. AuthenticationManager — entry point duy nhất
AuthenticationManager là interface entry point cho mọi yêu cầu xác thực trong Spring Security:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
Interface chỉ có một method duy nhất. Filter gọi manager.authenticate(unauthenticated) rồi nhận lại Authentication đã xác thực, hoặc bắt exception nếu thất bại.
Implementation mặc định là ProviderManager — bên trong nó giữ một danh sách AuthenticationProvider và thử lần lượt:
// Simplified logic in ProviderManager.authenticate()
for (AuthenticationProvider provider : this.providers) {
if (!provider.supports(authentication.getClass())) {
continue; // skip — not this provider's job
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
} catch (AccountStatusException | InternalAuthenticationServiceException ex) {
throw ex; // non-recoverable — stop immediately
} catch (AuthenticationException ex) {
lastException = ex; // try next provider
}
}
if (result == null) throw lastException;
Điểm quan trọng: ProviderManager không biết gì về JWT hay username/password — nó chỉ điều phối. Kiến thức về từng loại token nằm trong AuthenticationProvider. Provider đầu tiên vừa supports() vừa authenticate thành công sẽ thắng — các provider sau không được hỏi:
flowchart LR
PM["ProviderManager"] --> Dao{"Dao<br/>supports?"}
Dao -->|"co"| DaoOK["Dao.authenticate()<br/>first match wins -- dung lai"]
Dao -->|"khong"| Ldap{"Ldap<br/>supports?"}
Ldap -->|"co"| LdapOK["Ldap.authenticate()"]
Ldap -->|"khong"| Anon{"Anonymous<br/>supports?"}
Anon -->|"co"| AnonOK["Anonymous.authenticate()"]
Anon -->|"het provider"| Fail["throw lastException"]5. AuthenticationProvider — pluggable strategy
AuthenticationProvider là strategy cho từng loại token. Mỗi provider khai báo supports() để nói "tôi handle loại token nào":
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
Bảng provider phổ biến trong Spring Security:
| Provider | Token type | Dùng khi |
|---|---|---|
DaoAuthenticationProvider | UsernamePasswordAuthenticationToken | Form login, Basic Auth |
JwtAuthenticationProvider | BearerTokenAuthenticationToken | JWT Bearer token |
Saml2AuthenticationProvider | Saml2AuthenticationToken | SAML SSO |
| Custom | Custom token | OAuth2 client credential, API key |
Tách authentication thành nhiều provider độc lập cho phép app hỗ trợ đồng thời nhiều phương thức xác thực — cùng một chain có thể chấp nhận cả form login và JWT Bearer mà không có code rẽ nhánh if/else.
5.1 DaoAuthenticationProvider và UserDetailsService
Khi người dùng submit form login (username + password), DaoAuthenticationProvider cần tra cứu user từ database. Nó làm việc này qua UserDetailsService — interface một method:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
App triển khai UserDetailsService để kết nối với DB của mình. Xem chi tiết tại UserDetailsService & BCrypt.
6. SecurityContext + ThreadLocal — tại sao mỗi request có context riêng
Sau khi provider trả về Authentication đã xác thực, filter lưu nó vào SecurityContextHolder:
// In BearerTokenAuthenticationFilter (simplified)
Authentication authResult = authManager.authenticate(token);
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
chain.doFilter(request, response); // pass to next filter / controller
SecurityContextHolder.clearContext(); // cleanup — CRITICAL
SecurityContextHolder dùng ThreadLocal làm storage:
public class ThreadLocalSecurityContextHolderStrategy
implements SecurityContextHolderStrategy {
private static final ThreadLocal<Supplier<SecurityContext>> contextHolder =
new ThreadLocal<>();
@Override
public SecurityContext getContext() {
return getDeferredContext().get();
}
@Override
public void clearContext() {
contextHolder.remove(); // remove, not set null
}
}
6.1 Tại sao ThreadLocal?
Câu hỏi cốt lõi: tại sao Spring Security chọn ThreadLocal thay vì truyền Authentication qua method parameter từ filter xuống controller?
Lý do kiến trúc:
- Không sửa API hiện có: Servlet
Filter.doFilter()vàHttpServlet.service()có signature cố định — không thể thêm parameterAuthenticationvào mà không phá vỡ toàn bộ Servlet API.ThreadLocallà cách inject context mà không đụng đến signature. - Mỗi request chạy trên một thread riêng (mô hình thread-per-request của Tomcat): mỗi thread có vùng
ThreadLocalđộc lập → context của request Alice không bao giờ lẫn sang request Bob dù cùng chạy song song. - Transparent cho business code: controller không cần nhận
Authenticationqua parameter — framework tự inject qua@AuthenticationPrincipalhoặcSecurityContextHolder.getContext().
Hình dung:
Thread-1 (Alice's request) Thread-2 (Bob's request)
ThreadLocal["context"] = Alice ThreadLocal["context"] = Bob
... ...
getContext() → Alice getContext() → Bob ← riêng biệt
clearContext() clearContext()
6.2 Cleanup bắt buộc
clearContext() được gọi trong finally block sau khi chain hoàn thành. Nếu không clear, thread trả về pool vẫn còn SecurityContext cũ — request tiếp theo lấy thread đó ra sẽ đọc context của request trước. Đây là lỗ hổng bảo mật nghiêm trọng.
// Pattern đúng — cleanup trong finally
try {
SecurityContextHolder.setContext(context);
chain.doFilter(request, response);
} finally {
SecurityContextHolder.clearContext(); // ALWAYS, even if exception
}
6.3 Pitfall — ThreadLocal không propagate sang thread khác
ThreadLocal chỉ có hiệu lực trong thread đang chạy. Khi dùng @Async hoặc CompletableFuture, code chạy trên thread pool khác — SecurityContext biến mất:
// SAI — context null trong async thread
@Async
public void processOrder() {
// SecurityContextHolder.getContext().getAuthentication() == null ← NPE
}
// DUNG — capture truoc khi switch thread
@PostMapping("/orders")
public CompletableFuture<Void> placeOrder(
@AuthenticationPrincipal UserDetails user,
@RequestBody OrderRequest req
) {
String username = user.getUsername(); // capture tren request thread
return CompletableFuture.runAsync(() -> orderService.process(username, req));
}
Bài SecurityFilterChain DSL sẽ demo cách cấu hình DelegatingSecurityContextExecutor cho use case async cần truyền context.
7. Authorization — sau khi có Authentication
AuthorizationFilter là filter cuối cùng trong chain. Sau khi Authentication đã được lưu vào SecurityContextHolder, filter này đọc ra và kiểm tra:
// Simplified logic in AuthorizationFilter
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
AuthorizationDecision decision = authorizationManager.check(() -> authentication, request);
if (!decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
// else: call chain.doFilter() — pass through
AuthorizationManager được config trong SecurityFilterChain:
http.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/projects/**").authenticated()
.anyRequest().permitAll()
);
Tại sao tách authentication và authorization thành hai bước riêng?
Nếu gộp lại, một component vừa hỏi "bạn là ai?" vừa hỏi "bạn được làm gì?" — hai concern khác nhau, khó mở rộng độc lập. Tách rời cho phép:
- Thay đổi cơ chế xác thực (từ password sang JWT, sang SAML) mà không động vào authorization rule.
- Thêm authorization rule mới (từ RBAC sang ABAC) mà không cần biết token được validate ra sao.
- Hai filter chạy ở hai vị trí khác nhau trong chain — filter trước cần reject sớm nếu token không hợp lệ, không cần chờ đến authorization.
8. Exception flow — AuthenticationEntryPoint vs AccessDeniedHandler
ExceptionTranslationFilter nằm ngay trước AuthorizationFilter trong chain. Nhiệm vụ của nó là bắt hai loại exception và map sang HTTP response tương ứng:
flowchart TB
Req[Request]
AuthF[Authentication Filter]
ExTrans[ExceptionTranslationFilter]
AZ[AuthorizationFilter]
Ctrl[Controller]
Req --> AuthF
AuthF -->|"AuthenticationException<br/>(token invalid / expired)"| R401["401 Unauthorized<br/>AuthenticationEntryPoint"]
AuthF -->|"valid token"| ExTrans
ExTrans --> AZ
AZ -->|"AccessDeniedException<br/>(no permission)"| R403["403 Forbidden<br/>AccessDeniedHandler"]
AZ -->|"granted"| CtrlHai handler chịu trách nhiệm sinh HTTP response:
| Exception | Handler | HTTP Status | Khi nào |
|---|---|---|---|
AuthenticationException | AuthenticationEntryPoint | 401 Unauthorized | Token không hợp lệ, hết hạn, thiếu |
AccessDeniedException | AccessDeniedHandler | 403 Forbidden | Token hợp lệ nhưng không có quyền |
Với REST API dùng JWT, cần config handler trả JSON thay vì redirect về form login:
http.exceptionHandling(ex -> ex
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"Unauthorized\"}");
})
.accessDeniedHandler((request, response, accessDeniedException) -> {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"Forbidden\"}");
})
);
Nếu không config, Spring Security mặc định redirect đến /login khi gặp 401 — đúng cho web app form login, nhưng sai cho REST API (frontend nhận 302, không phải 401).
8.1 Anonymous user
Khi request không có token, AnonymousAuthenticationFilter tự động đặt vào SecurityContextHolder một AnonymousAuthenticationToken với authority ROLE_ANONYMOUS. Điều này giúp AuthorizationFilter xử lý nhất quán — luôn có Authentication trong context, không bao giờ null. Khi anonymous user truy cập endpoint protected, AuthorizationFilter ném AccessDeniedException; ExceptionTranslationFilter phát hiện đây là anonymous nên gọi AuthenticationEntryPoint (401) thay vì AccessDeniedHandler (403).
9. Cơ chế bên dưới — luồng đầy đủ một request JWT
Gộp tất cả lại, đây là trình tự chính xác từ byte đầu của request cho đến controller:
1. Tomcat nhận HTTP request, dispatch sang thread từ pool
2. DelegatingFilterProxy.doFilter() → lazy-fetch FilterChainProxy bean
3. FilterChainProxy match URL → chọn SecurityFilterChain
4. SecurityContextHolderFilter: load SecurityContext từ session hoặc tạo mới → lưu ThreadLocal
5. CsrfFilter: check CSRF token (nếu enabled)
6. BearerTokenAuthenticationFilter:
a. Đọc header Authorization: Bearer <token>
b. Tạo BearerTokenAuthenticationToken(token) — isAuthenticated() = false
c. Gọi AuthenticationManager.authenticate(token)
d. ProviderManager iterate providers:
- JwtAuthenticationProvider.supports(BearerTokenAuthenticationToken) = true
- JwtAuthenticationProvider.authenticate(): decode JWT, verify signature, check expiry
- Trả về JwtAuthenticationToken — isAuthenticated() = true, authorities = [ROLE_USER]
e. SecurityContextHolder.setContext(context với JwtAuthenticationToken)
f. chain.doFilter() — tiếp tục
7. AnonymousAuthenticationFilter: context đã có auth → skip
8. ExceptionTranslationFilter: wrap chain trong try/catch
9. AuthorizationFilter:
a. Đọc Authentication từ SecurityContextHolder
b. AuthorizationManager.check(): Alice có ROLE_USER, endpoint yêu cầu authenticated() → granted
c. chain.doFilter() — tiếp tục
10. DispatcherServlet → Controller nhận request
11. Controller có thể đọc auth qua @AuthenticationPrincipal hoặc SecurityContextHolder
12. Response trả về, SecurityContextHolder.clearContext() trong finally
10. Pitfall
Nhầm 1 — Không config AuthenticationEntryPoint cho REST API:
// SAI — default Spring Security redirect 302 /login khi token expired
// Frontend nhận 302 Not Modified thay vi 401 Unauthorized
// DUNG — override handler tra JSON
http.exceptionHandling(ex -> ex
.authenticationEntryPoint(new BearerTokenAuthenticationEntryPoint())
.accessDeniedHandler(new BearerTokenAccessDeniedHandler())
);
// BearerTokenAuthenticationEntryPoint tra 401 + WWW-Authenticate header
// BearerTokenAccessDeniedHandler tra 403 + WWW-Authenticate header
Nhầm 2 — Tưởng SecurityContextHolder.getContext() trả null khi không có user:
// SAI assumption
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) { ... } // dung — auth co the null truoc khi set
// DUNG — kiem tra ca null va isAuthenticated
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth != null && auth.isAuthenticated()
&& !(auth instanceof AnonymousAuthenticationToken)) {
// da xac thuc that su
}
Nhầm 3 — Gọi SecurityContextHolder trong constructor của Spring bean:
Bean được khởi tạo lúc application startup — trước khi bất kỳ request nào đến. SecurityContextHolder chưa có context lúc đó. Đọc SecurityContext trong constructor luôn trả null.
Liên hệ các bài khác
- 01 — Filter chain architecture: bài này đã mô tả cấu trúc ba tầng
DelegatingFilterProxy→FilterChainProxy→SecurityFilterChainvà danh sách 15+ filter trong chain — nền để hiểu tại saoBearerTokenAuthenticationFiltervàExceptionTranslationFilternằm ở vị trí đó. - UserDetailsService & BCrypt:
DaoAuthenticationProvider(form login) dùngUserDetailsServiceđể tra cứu user từ DB vàPasswordEncoderđể verify password hash — phần bổ sung cho authentication flow bài này khi dùng username/password thay JWT. - 03 — SecurityFilterChain DSL: DSL
http.authorizeHttpRequests(),http.oauth2ResourceServer(), vàhttp.exceptionHandling()là cách khai báo config cho filter chain và exception handler đã đề cập ở đây.
Tóm tắt
BearerTokenAuthenticationFilterđọc header, xâyAuthenticationchưa xác thực (input), giao choAuthenticationManager.ProviderManagerđiều phối danh sáchAuthenticationProvider— mỗi provider handle một loại token quasupports().- Provider trả về
Authenticationđã xác thực (output):isAuthenticated() = true, credentials =null, authorities đầy đủ. SecurityContextHolderlưuAuthenticationvàoThreadLocalper-thread — mỗi request có context riêng, không lẫn nhau;clearContext()trongfinallylà bắt buộc.ThreadLocalđược chọn vì Servlet API không cho phép truyềnAuthenticationqua parameter; thread-per-request của Tomcat đảm bảo isolation.- Authentication và authorization tách rời: filter authenticate trước,
AuthorizationFiltercheck authority sau — hai concern độc lập, dễ mở rộng riêng. ExceptionTranslationFiltermap:AuthenticationException→ 401 (AuthenticationEntryPoint),AccessDeniedException→ 403 (AccessDeniedHandler). REST API cần override cả hai để trả JSON thay redirect.
Tự kiểm tra
Q1Tại sao ProviderManager giữ danh sách AuthenticationProvider thay vì một provider duy nhất? Lợi ích thiết kế là gì?▸
ProviderManager giữ danh sách AuthenticationProvider thay vì một provider duy nhất? Lợi ích thiết kế là gì?Mỗi phương thức xác thực yêu cầu logic khác nhau: JWT cần verify chữ ký RSA/HMAC + decode claims; username/password cần tra DB + so sánh BCrypt hash; SAML cần parse XML assertion. Nếu gộp vào một class, nó sẽ vi phạm Single Responsibility Principle và phình to theo mỗi phương thức mới.
Tách thành nhiều provider độc lập cho phép app hỗ trợ đồng thời nhiều phương thức xác thực. Mỗi provider khai báo supports(Class<?>) để nói rõ loại token nào mình xử lý — ProviderManager chỉ làm nhiệm vụ điều phối.
Hệ quả thực tế: thêm phương thức xác thực mới (ví dụ API key) chỉ cần thêm một AuthenticationProvider mới và đăng ký vào ProviderManager, không cần sửa bất kỳ provider hiện có nào. Đây là Open/Closed Principle áp dụng vào security.
Q2Giải thích tại sao Spring Security dùng ThreadLocal để lưu SecurityContext thay vì truyền Authentication qua method parameter từ filter xuống controller.▸
ThreadLocal để lưu SecurityContext thay vì truyền Authentication qua method parameter từ filter xuống controller.Servlet API có signature cố định từ thập niên 1990: Filter.doFilter(ServletRequest, ServletResponse, FilterChain) và HttpServlet.service(HttpServletRequest, HttpServletResponse). Không thể thêm parameter Authentication vào signature đó mà không phá vỡ toàn bộ Servlet API và mọi framework build trên nó.
ThreadLocal là cách inject context xuyên suốt call stack mà không cần truyền parameter: thread nào cũng có vùng ThreadLocal riêng, truy cập qua static method SecurityContextHolder.getContext() ở bất kỳ đâu trong thread.
Tính chất thread-isolation của ThreadLocal map trực tiếp lên mô hình thread-per-request của Tomcat: mỗi request chạy trên một thread riêng, nên context của Alice không bao giờ đọc được từ thread xử lý request của Bob, dù cả hai đang chạy song song.
Q3Phân biệt AuthenticationException và AccessDeniedException. Mỗi loại exception được xử lý bởi component nào và sinh ra HTTP status code gì?▸
AuthenticationException và AccessDeniedException. Mỗi loại exception được xử lý bởi component nào và sinh ra HTTP status code gì?AuthenticationException xảy ra trong quá trình xác thực — token không hợp lệ, hết hạn, chữ ký sai, hoặc user không tồn tại. Tại thời điểm này, Spring Security chưa biết danh tính của request. ExceptionTranslationFilter bắt exception này và gọi AuthenticationEntryPoint → sinh HTTP 401 Unauthorized.
AccessDeniedException xảy ra sau khi xác thực thành công — token hợp lệ, danh tính đã biết, nhưng user không có authority cần thiết để truy cập resource. ExceptionTranslationFilter bắt exception này và gọi AccessDeniedHandler → sinh HTTP 403 Forbidden.
Phân biệt 401 vs 403 quan trọng cho client: 401 nghĩa là "hãy cung cấp credential hoặc refresh token", 403 nghĩa là "dù bạn login đúng, bạn không được phép làm điều này". REST API cần config cả hai handler để trả JSON, vì mặc định Spring Security redirect về form /login.
Q4Tại sao authentication và authorization được thiết kế thành hai bước riêng biệt trong Spring Security chain, thay vì gộp vào một component?▸
Authentication trả lời câu hỏi "bạn là ai?" — cần validate credential (JWT signature, BCrypt hash), tra DB, decode token. Authorization trả lời câu hỏi "bạn được làm gì?" — cần so sánh authority của user với rule được config cho endpoint. Hai concern hoàn toàn độc lập.
Tách rời cho phép thay thế hoặc mở rộng từng phần độc lập: chuyển từ username/password sang JWT không cần sửa authorization rule; thêm ABAC (phân quyền theo attribute) không cần biết token được validate ra sao. Nếu gộp lại, mỗi thay đổi ở một concern sẽ rủi ro ảnh hưởng concern kia.
Về vị trí trong chain, authentication filter chạy sớm hơn để có thể reject request với token không hợp lệ ngay lập tức — không cần tiêu tốn tài nguyên xử lý business logic. AuthorizationFilter chạy cuối, sau khi authentication context đã sẵn sàng trong ThreadLocal.
Q5Đoạn code sau có lỗi bảo mật tiềm ẩn liên quan đến ThreadLocal. Giải thích lỗi và cách sửa.
SecurityContextHolder.setContext(context);
chain.doFilter(request, response);
// no clearContext()▸
ThreadLocal. Giải thích lỗi và cách sửa.SecurityContextHolder.setContext(context);chain.doFilter(request, response);// no clearContext()Lỗi là thiếu SecurityContextHolder.clearContext() sau khi chain hoàn thành. Tomcat dùng thread pool — sau khi xử lý xong request, thread không bị huỷ mà trả về pool để phục vụ request tiếp theo. Nếu ThreadLocal không được clear, thread vẫn còn SecurityContext của request Alice.
Khi request tiếp theo (Bob) lấy thread đó từ pool, Bob có thể đọc được SecurityContext của Alice — tức là Bob sẽ được xác thực với danh tính của Alice, không phải danh tính của chính Bob. Đây là lỗ hổng privilege escalation nghiêm trọng.
Cách sửa là đặt clearContext() trong finally block để đảm bảo luôn được gọi dù chain ném exception:
try {
SecurityContextHolder.setContext(context);
chain.doFilter(request, response);
} finally {
SecurityContextHolder.clearContext();
}Spring Security đã làm điều này trong tất cả filter built-in. Nếu tự viết custom filter cần set SecurityContext, bắt buộc phải follow pattern này.
Bài tiếp theo: SecurityFilterChain DSL
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên