Spring Security & Testing/Authentication flow & SecurityContext — từ request đến principal trong ThreadLocal
3/20
Bài 3 / 20~12 phútSecurity Architecture & Filter ChainMiễn phí lượt xem

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

  1. Filter nào trong chain chịu trách nhiệm đọc header đó?
  2. Token được validate ở đâu, bằng cơ chế gì?
  3. "Alice đã login" được lưu ở đâu để controller sau đó đọc được?
  4. 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: JwtAuthenticationToken

Ba 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áiisAuthenticated()getPrincipal()getCredentials()getAuthorities()
Trước xác thực (input)falseusername / raw tokenpassword / token rawrỗng
Sau xác thực (output)trueUserDetails objectnull (đã 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"| E

Lý 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:

ProviderToken typeDùng khi
DaoAuthenticationProviderUsernamePasswordAuthenticationTokenForm login, Basic Auth
JwtAuthenticationProviderBearerTokenAuthenticationTokenJWT Bearer token
Saml2AuthenticationProviderSaml2AuthenticationTokenSAML SSO
CustomCustom tokenOAuth2 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 DaoAuthenticationProviderUserDetailsService

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()HttpServlet.service() có signature cố định — không thể thêm parameter Authentication vào mà không phá vỡ toàn bộ Servlet API. ThreadLocal là 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 Authentication qua parameter — framework tự inject qua @AuthenticationPrincipal hoặc SecurityContextHolder.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"| Ctrl

Hai handler chịu trách nhiệm sinh HTTP response:

ExceptionHandlerHTTP StatusKhi nào
AuthenticationExceptionAuthenticationEntryPoint401 UnauthorizedToken không hợp lệ, hết hạn, thiếu
AccessDeniedExceptionAccessDeniedHandler403 ForbiddenToken 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 DelegatingFilterProxyFilterChainProxySecurityFilterChain và danh sách 15+ filter trong chain — nền để hiểu tại sao BearerTokenAuthenticationFilterExceptionTranslationFilter nằm ở vị trí đó.
  • UserDetailsService & BCrypt: DaoAuthenticationProvider (form login) dùng UserDetailsService để 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ây Authentication chưa xác thực (input), giao cho AuthenticationManager.
  • ProviderManager điều phối danh sách AuthenticationProvider — mỗi provider handle một loại token qua supports().
  • Provider trả về Authentication đã xác thực (output): isAuthenticated() = true, credentials = null, authorities đầy đủ.
  • SecurityContextHolder lưu Authentication vào ThreadLocal per-thread — mỗi request có context riêng, không lẫn nhau; clearContext() trong finally là bắt buộc.
  • ThreadLocal được chọn vì Servlet API không cho phép truyền Authentication qua parameter; thread-per-request của Tomcat đảm bảo isolation.
  • Authentication và authorization tách rời: filter authenticate trước, AuthorizationFilter check authority sau — hai concern độc lập, dễ mở rộng riêng.
  • ExceptionTranslationFilter map: AuthenticationException → 401 (AuthenticationEntryPoint), AccessDeniedException → 403 (AccessDeniedHandler). REST API cần override cả hai để trả JSON thay redirect.

Tự kiểm tra

Tự kiểm tra
Q1
Tại sao 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.

Q2
Giả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.

Servlet API có signature cố định từ thập niên 1990: Filter.doFilter(ServletRequest, ServletResponse, FilterChain)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.

Q3
Phân biệt AuthenticationExceptionAccessDeniedException. 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.

Q4
Tạ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()

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

Đặt 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