Luồng Xác Thực và SecurityContext Trong ThreadLocal
Giải mã luồng xác thực Spring Security: vai trò AuthenticationManager, ProviderManager và lưu trữ SecurityContext an toàn bằng ThreadLocal.
TL;DR: Khi request mang header Authorization: Bearer <token> đi vào chuỗi bảo mật, BearerTokenAuthenticationFilter trích xuất token, tạo đối tượng Authentication chưa xác thực và chuyển giao cho AuthenticationManager. ProviderManager kích hoạt AuthenticationProvider phù hợp để giải mã và kiểm tra chữ ký. Khi thành công, đối tượng Authentication đã xác thực được lưu vào SecurityContextHolder thông qua ThreadLocal. Ở cuối chuỗi, AuthorizationFilter đọc danh tính để kiểm tra quyền, còn ExceptionTranslationFilter phân luồng lỗi: chưa xác thực trả HTTP 401, thiếu quyền trả HTTP 403.
Kịch bản một JWT request đi vào hệ thống như thế nào?
Xét một ứng dụng REST API nhận request với header xác thực:
GET /api/projects HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9.payload.sig
Hệ thống bảo mật cần trả lời 4 câu hỏi then chốt:
- Filter nào chịu trách nhiệm đọc header này?
- Token được thẩm định tính hợp lệ ở đâu và qua cơ chế nào?
- Danh tính của người dùng được lưu trữ ở đâu để Controller có thể truy xuất?
- Nếu token hết hạn, phản hồi lỗi 401 được khởi tạo tại điểm nào trong chuỗi?
Toàn cảnh luồng xác thực diễn ra qua những mắt xích nào?
Quá trình thẩm định danh tính và cấp quyền di chuyển qua một đường ống khép kín với hai nhánh thoát ngoại lệ:

Ba thành phần cốt lõi điều phối toàn bộ chu trình là AuthenticationManager, AuthenticationProvider, và kho lưu trữ ngữ cảnh SecurityContextHolder.
Đối tượng Authentication biến chuyển qua hai trạng thái ra sao?
Giao diện Authentication đại diện cho danh tính của người dùng xuyên suốt chu kỳ request:
public interface Authentication {
String getName();
Object getPrincipal(); // UserDetails or JWT subject
Object getCredentials(); // Password or raw token (cleared after auth)
Collection<? extends GrantedAuthority> getAuthorities();
boolean isAuthenticated();
Object getDetails(); // Request metadata (IP, session)
}
Trong vòng đời của một truy vấn, Authentication tồn tại ở hai trạng thái rõ rệt:
| Thuộc tính | Trước khi xác thực (Input) | Sau khi xác thực thành công (Output) |
|---|---|---|
isAuthenticated() | false | true |
getPrincipal() | Username hoặc chuỗi token thô | Đối tượng UserDetails hoặc JWT Claims |
getCredentials() | Password thô hoặc token thô | null (đã xóa vì an toàn bộ nhớ) |
getAuthorities() | Danh sách rỗng [] | Tập hợp các Role / Permission được cấp |

Cơ chế xóa credentials (eraseCredentialsAfterAuthentication = true) giúp ngăn ngừa rủi ro rò rỉ mật khẩu hoặc chuỗi token thô khi memory dump bị trích xuất.
AuthenticationManager điều phối các chiến lược xác thực thế nào?
AuthenticationManager là đầu mối tiếp nhận duy nhất cho mọi yêu cầu xác thực:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
Lớp triển khai mặc định là ProviderManager. Nó không trực tiếp giải mã token mà chứa danh sách các AuthenticationProvider và điều phối theo cơ chế thử tuần tự:
for (AuthenticationProvider provider : this.providers) {
if (!provider.supports(authentication.getClass())) {
continue; // Skip if provider cannot handle this token type
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break; // Stop at first successful provider
}
} catch (AuthenticationException ex) {
lastException = ex;
}
}

AuthenticationProvider đóng vai trò gì trong kiến trúc mở rộng?
Mỗi AuthenticationProvider là một chiến lược xác thực độc lập, khai báo rõ ràng năng lực xử lý thông qua phương thức supports():
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
boolean supports(Class<?> authentication);
}
Nhờ mô hình cắm-rút (pluggable strategy), một ứng dụng có thể hỗ trợ đồng thời đăng nhập Form Login truyền thống (DaoAuthenticationProvider), xác thực Bearer JWT (JwtAuthenticationProvider) và đăng nhập đơn SAML/OAuth2 mà không làm phát sinh mã rẽ nhánh lộn xộn.
Tại sao SecurityContext lại được lưu trữ trong ThreadLocal?
Sau khi xác thực thành công, kết quả được lưu trữ vào SecurityContextHolder:
SecurityContext context = SecurityContextHolder.createEmptyContext();
context.setAuthentication(authResult);
SecurityContextHolder.setContext(context);
Mặc định, SecurityContextHolder lưu trữ ngữ cảnh trong ThreadLocal:
- Không thay đổi chữ ký Servlet API: Chuẩn
Filter.doFilter()không thể truyền thêm tham số danh tính.ThreadLocalcho phép truy cập danh tính ở bất kỳ tầng nào mà không cần truyền parameter. - Cô lập theo thread (Thread Isolation): Trong mô hình thread-per-request của Tomcat, mỗi request được phục vụ bởi một thread riêng. Ngữ cảnh của người dùng A hoàn toàn tách biệt với người dùng B.
- Trong suốt với tầng nghiệp vụ: Controller có thể truy xuất dữ liệu an toàn thông qua annotation
@AuthenticationPrincipal.
Do Tomcat tái sử dụng luồng từ thread pool, nếu không gọi clearContext() trong khối finally, thread được trả về pool sẽ mang theo danh tính cũ, dẫn đến lỗ hổng leo thang đặc quyền (privilege escalation) cho request tiếp theo.
try {
SecurityContextHolder.setContext(context);
chain.doFilter(request, response);
} finally {
SecurityContextHolder.clearContext(); // Clean up thread local
}
Phân quyền Authorization diễn ra sau khi có danh tính ra sao?
AuthorizationFilter đứng ở cuối chuỗi filter. Khi danh tính đã được thiết lập ổn định trong SecurityContextHolder, bộ lọc này lấy thông tin quyền hạn ra và đối soát với quy tắc truy cập của URL:
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
AuthorizationDecision decision = authorizationManager.check(() -> auth, request);
if (!decision.isGranted()) {
throw new AccessDeniedException("Access Denied");
}
chain.doFilter(request, response);
Việc phân tách rạch ròi giữa Xác thực (Authentication — bạn là ai?) và Phân quyền (Authorization — bạn được làm gì?) giúp hệ thống dễ dàng thay đổi phương thức định danh mà không cần viết lại ma trận quyền hạn.
ExceptionTranslationFilter xử lý lỗi 401 và 403 như thế nào?
Bộ lọc ExceptionTranslationFilter bọc quanh chuỗi để bắt các ngoại lệ bảo mật và ủy quyền cho hai thành phần chuyên trách:
| Ngoại lệ | Thành phần phản hồi | Mã HTTP Status | Tình huống kích hoạt |
|---|---|---|---|
AuthenticationException | AuthenticationEntryPoint | 401 Unauthorized | Thiếu token, token sai chữ ký hoặc hết hạn |
AccessDeniedException | AccessDeniedHandler | 403 Forbidden | Token hợp lệ nhưng thiếu Role / Scope truy cập |
Đối với các ứng dụng REST API stateless, ta cấu hình trả về định dạng JSON thay vì hành vi redirect mặc định của form login:
http.exceptionHandling(ex -> ex
.authenticationEntryPoint((req, res, err) -> {
res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
res.setContentType("application/json");
res.getWriter().write("{\"error\": \"Unauthorized\", \"message\": \"" + err.getMessage() + "\"}");
})
.accessDeniedHandler((req, res, err) -> {
res.setStatus(HttpServletResponse.SC_FORBIDDEN);
res.setContentType("application/json");
res.getWriter().write("{\"error\": \"Forbidden\", \"message\": \"Access denied\"}");
})
);
Toàn bộ chu kỳ một request JWT từ A đến Z diễn ra ra sao?
- Tomcat Dispatch: Tiếp nhận HTTP request, phân bổ một thread từ thread pool.
- DelegatingFilterProxy: Định tuyến tới
FilterChainProxy. - SecurityContextHolderFilter: Khởi tạo
SecurityContextrỗng trongThreadLocal. - BearerTokenAuthenticationFilter: Trích xuất token từ header, tạo
BearerTokenAuthenticationToken(unauthenticated). - ProviderManager: Điều phối tới
JwtAuthenticationProvider, kiểm tra chữ ký và claims, trả vềJwtAuthenticationToken(authenticated). - Lưu trữ Context: Đặt đối tượng xác thực vào
SecurityContextHolder. - ExceptionTranslationFilter: Thiết lập khối try/catch bắt ngoại lệ bảo mật.
- AuthorizationFilter: Đối chiếu danh sách quyền với quy tắc bảo mật của URL.
- Controller thực thi: DispatcherServlet gọi controller method và inject
@AuthenticationPrincipal. - Cleanup: Sau khi gửi response, phương thức
clearContext()được gọi trong khốifinally.
Những cạm bẫy thường gặp khi xử lý Authentication flow là gì?
- Quên cấu hình EntryPoint cho REST API: Mặc định Spring Security sẽ redirect mã 302 về trang
/loginthay vì trả mã 401 JSON cho client di động hoặc SPA. - Mất Context trong Async Task: Do
ThreadLocalkhông tự động sao chép sang các luồng con của@AsynchoặcCompletableFuture, cần dùngDelegatingSecurityContextExecutorkhi cần chuyển giao context. - Đọc Context trong Constructor: Constructor của bean chạy lúc ứng dụng khởi động khi chưa có request nào đến, việc gọi
SecurityContextHolder.getContext()tại đây sẽ luôn trả vềnull.
Những điểm then chốt cần ghi nhớ là gì?
AuthenticationManagergiữ vai trò điều phối thông quaProviderManager, cácAuthenticationProviderthực hiện thẩm định cụ thể.Authenticationchuyển từ trạng thái thô sang trạng thái xác thực đầy đủ kèm theo hành động xóa sạch credentials.SecurityContextlưu trữ danh tính trongThreadLocal, đảm bảo tính cô lập giữa các request và bắt buộc phải được giải phóng sau mỗi chu kỳ.- Phân biệt rõ ràng giữa mã lỗi 401 (lỗi định danh) và mã lỗi 403 (lỗi cấp quyền) để phản hồi chuẩn xác cho client.
Bạn đã nắm vững luồng xác thực và SecurityContext chưa?
- Q1Tại sao ProviderManager lại ủy quyền cho nhiều AuthenticationProvider thay vì tự mình xử lý xác thực?
- Q2Hành động nào được thực hiện đối với trường credentials trong đối tượng Authentication sau khi xác thực thành công?
- Q3Lý do quan trọng nhất khiến Spring Security chọn ThreadLocal làm nơi lưu trữ SecurityContext là gì?
- Q4Điều gì sẽ xảy ra nếu một custom filter quên gọi SecurityContextHolder.clearContext() trong khối finally?
- Q5Ngoại lệ nào sau đây sẽ kích hoạt AuthenticationEntryPoint để trả về mã phản hồi HTTP 401 Unauthorized?
Bài tiếp theo: SecurityFilterChain lambda DSL
Bài này có giúp bạn hiểu bản chất không?
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Kiến Trúc Filter Chain: Cơ Chế Bảo Vệ Trước Servlet
Bài tiếpCấu Hình SecurityFilterChain Bằng Lambda DSL Hiện Đại
Lộ trình Junior Java Backend Developer
Chặng 18/21 · Authentication & Authorization — bài 3/19 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này