Filter chain: Spring Security chặn request ở đâu?
Mình muốn học Spring Security.
Chúng ta học Spring Security theo hướng thực tế với Spring Boot 3 / Spring Security 6. Cách học: Bài toán → Vấn đề → Giải pháp → Code → Cơ chế → Câu hỏi phỏng vấn.
Lộ trình:
1. Filter chain (bài này)
2. Authentication flow AuthenticationManager, UserDetailsService, BCrypt
3. Authorization role, authority, 401 vs 403
4. Session login JSESSIONID, session fixation, Spring Session Redis
5. JWT stateless, Bearer token, revoke
6+. Refresh token, OAuth2/OIDC, Keycloak, CORS/CSRF...
1. Bài toán: ai cũng xóa được đơn hàng
Project ví dụ xuyên suốt: shop-app, một REST API quản lý đơn hàng (Spring Boot 3, Java 17).
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping
public String getOrders() {
return "Order list";
}
@DeleteMapping("/{id}")
public String deleteOrder(@PathVariable Long id) {
return "Order deleted";
}
}
Hiện tại bất kỳ ai cũng gọi được:
GET /api/orders
DELETE /api/orders/100
2. Vấn đề
Người chưa đăng nhập vẫn xóa được đơn hàng. Ta cần hai cơ chế:
- Authentication (xác thực): bạn là ai? Đã đăng nhập chưa?
- Authorization (phân quyền): bạn được làm gì?
Nếu tự viết if (user == null) trong từng Controller thì lặp code, dễ quên một endpoint, và lúc đó request đã đi tới tận Controller rồi.
3. Giải pháp: chặn từ tầng Servlet Filter
Spring Security không phải Controller riêng. Nó cắm vào chuỗi Servlet Filter của Tomcat, nên kiểm tra request trước khi request tới DispatcherServlet.
HTTP Request
|
v
Tomcat
|
v
Servlet Filters
|
+-- Spring Security
| +-- SecurityFilterChain
| +-- Authentication
| +-- Authorization
v
DispatcherServlet
|
v
Controller → Service → Repository
Ba thành phần cần phân biệt:
| Thành phần | Là gì | Ai sở hữu |
|---|---|---|
DelegatingFilterProxy | Một Servlet Filter "cầu nối". Tomcat biết nó, nó chuyển việc sang một bean trong Spring context | Servlet container |
FilterChainProxy | Bean tên springSecurityFilterChain. Chọn SecurityFilterChain phù hợp với request rồi chạy các filter trong đó | Spring context |
SecurityFilterChain | Một danh sách filter bảo mật + điều kiện áp dụng (request matcher). Bạn khai báo bằng @Bean | Code của bạn |
Vì sao cần DelegatingFilterProxy? Tomcat đăng ký filter trước khi Spring tạo xong bean. Proxy cho phép filter thật là một Spring bean (được inject, cấu hình bằng Java), được lấy ra lúc request đầu tiên đến.
4. Code từng bước
Bước 1: Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Chỉ cần thêm dependency, Spring Boot đã khóa mọi endpoint, tạo user user với password ngẫu nhiên in ra log. Đó là cấu hình mặc định, chỉ để thử.
Bước 2: Khai báo SecurityFilterChain bean. Spring Security 6 không còn WebSecurityConfigurerAdapter; ta khai báo bean:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
| Cấu hình | Chức năng |
|---|---|
permitAll() | Không yêu cầu đăng nhập |
authenticated() | Phải xác thực |
hasRole("ADMIN") | Phải có authority ROLE_ADMIN |
httpBasic() | Bật HTTP Basic (thêm BasicAuthenticationFilter) |
http.build() | Tạo ra SecurityFilterChain |
Bước 3: Xem chain có những filter nào
# application.properties
logging.level.org.springframework.security=DEBUG
Lúc khởi động, log in danh sách filter theo thứ tự (rút gọn):
DisableEncodeUrlFilter
WebAsyncManagerIntegrationFilter
SecurityContextHolderFilter ← nạp SecurityContext cho request
HeaderWriterFilter ← security headers
CsrfFilter ← kiểm CSRF token
LogoutFilter
UsernamePasswordAuthenticationFilter (nếu bật formLogin)
BasicAuthenticationFilter ← đọc header Authorization: Basic
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter ← chưa ai đăng nhập thì gắn "anonymous"
ExceptionTranslationFilter ← đổi exception thành 401/403
AuthorizationFilter ← quyết định cho qua hay không (cuối cùng)
Bước 4: Test bằng curl
curl -i http://localhost:8080/api/public/products
# 200 OK
curl -i http://localhost:8080/api/orders
# 401 Unauthorized (chưa gửi credentials)
curl -i -u john:<password> http://localhost:8080/api/admin/users
# 403 Forbidden (đăng nhập rồi nhưng không phải ADMIN)
5. Cách hoạt động
Tomcat filter chain
├── CharacterEncodingFilter
├── DelegatingFilterProxy ("springSecurityFilterChain")
│ │ lấy bean từ ApplicationContext
│ v
│ FilterChainProxy
│ │ duyệt các SecurityFilterChain theo thứ tự,
│ │ chain ĐẦU TIÊN có matcher khớp request sẽ được dùng
│ v
│ SecurityFilterChain #n
│ ├── SecurityContextHolderFilter
│ ├── CsrfFilter
│ ├── BasicAuthenticationFilter
│ ├── ...
│ ├── ExceptionTranslationFilter
│ └── AuthorizationFilter
│ │
│ v (nếu được cho qua)
└── DispatcherServlet → Controller
Điểm hay hỏi: chỉ một SecurityFilterChain được chạy cho mỗi request, là chain đầu tiên khớp. FilterChainProxy còn làm vài việc chung: dọn SecurityContextHolder sau request (tránh rò rỉ sang thread khác trong pool), áp dụng HttpFirewall chặn URL lạ.
6. So sánh
| Servlet Filter thường | Filter trong SecurityFilterChain | |
|---|---|---|
| Đăng ký với | Tomcat | FilterChainProxy (Spring) |
| Áp dụng theo URL | url-pattern đơn giản | RequestMatcher linh hoạt |
| Inject bean | Khó | Dễ, là Spring bean |
| Nhiều cấu hình khác nhau | Không | Nhiều chain, mỗi chain một luật |
7. Khi nào nên dùng / không nên dùng
- Dùng cấu hình URL trong
SecurityFilterChaincho luật thô: public, admin, authenticated. - Dùng nhiều chain khi các nhóm URL cần cơ chế xác thực khác hẳn nhau (API dùng JWT, trang admin dùng form login).
- Không nhét business logic (kiểm tra đơn hàng của ai) vào filter. Việc đó để ở Service (bài 4.4.3).
- Không tắt CSRF chỉ vì "copy trên mạng". Chỉ tắt khi API thật sự stateless và không dùng cookie (bài 4.4.4, 4.4.5).
Vậy một app có nhiều SecurityFilterChain được không? Ví dụ /api dùng token còn /admin dùng form login.
Được, và đây là cách làm chuẩn. Mỗi chain có securityMatcher riêng và thứ tự bằng @Order. Chain cụ thể đặt trước, chain "bắt tất cả" đặt cuối:
@Configuration
public class SecurityConfig {
@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain webChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().permitAll())
.formLogin(Customizer.withDefaults());
return http.build();
}
}
GET /api/orders → khớp apiChain (Order 1) → chạy apiChain, BỎ QUA webChain
GET /admin/report → không khớp apiChain → thử webChain → khớp
Lỗi hay gặp: chain không có securityMatcher (khớp mọi request) lại đặt @Order nhỏ hơn. Khi đó nó "nuốt" hết, chain sau không bao giờ chạy.
Còn requestMatchers với securityMatcher khác nhau chỗ nào? Nhìn giống nhau quá.
Bạn hiểu gần đúng là cả hai đều so URL, chỉ cần chỉnh một điểm: chúng hoạt động ở hai cấp khác nhau.
securityMatcher | requestMatchers (trong authorizeHttpRequests) | |
|---|---|---|
| Trả lời câu hỏi | Chain này có xử lý request không? | Request này cần quyền gì? |
| Cấp | Chọn chain (FilterChainProxy) | Bên trong AuthorizationFilter |
| Không khớp thì | Chuyển sang chain kế tiếp | Thử luật kế tiếp; luật đầu tiên khớp thắng |
Thứ tự requestMatchers cũng quan trọng: luật cụ thể trước, anyRequest() luôn cuối cùng. Nếu đặt .requestMatchers("/api/**").authenticated() trước .requestMatchers("/api/public/**").permitAll() thì /api/public vẫn bị đòi đăng nhập.
Tôi muốn thêm filter của riêng mình (log request, đọc token) thì gắn vào đâu?
Đúng rồi, đây là chỗ bạn sẽ dùng khi làm JWT tự viết. Kế thừa OncePerRequestFilter (đảm bảo chạy một lần/request) và chèn vào vị trí cụ thể trong chain:
public class RequestLogFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res,
FilterChain chain) throws ServletException, IOException {
long start = System.currentTimeMillis();
try {
chain.doFilter(req, res);
} finally {
log.info("{} {} -> {} ({} ms)", req.getMethod(), req.getRequestURI(),
res.getStatus(), System.currentTimeMillis() - start);
}
}
}
http.addFilterBefore(new RequestLogFilter(), UsernamePasswordAuthenticationFilter.class);
Lưu ý: nếu bạn khai báo filter đó là @Component, Spring Boot sẽ tự đăng ký thêm nó vào Tomcat, nên nó chạy hai lần (một trong Tomcat, một trong security chain). Hoặc không đánh @Component, hoặc tắt đăng ký bằng FilterRegistrationBean.setEnabled(false).
Phỏng vấn hỏi về filter chain của Spring Security thì trả lời sao?
Q1. What is Spring Security?
🇻🇳 Tiếng Việt:
Spring Security là framework lo authentication, authorization và chống các tấn công phổ biến (CSRF, session fixation...) cho ứng dụng Java. Nó dùng chuỗi Servlet Filter để chặn và kiểm tra request trước khi request tới Controller.
🇬🇧 English:
Spring Security is a framework that provides authentication, authorization, and protection against common attacks for Java applications. It uses a filter chain to intercept and secure HTTP requests before they reach the application controllers.
Q2. Giải thích DelegatingFilterProxy và FilterChainProxy.
🇻🇳 Tiếng Việt:
DelegatingFilterProxylà Servlet Filter đăng ký với Tomcat, nó ủy quyền cho beanspringSecurityFilterChaintrong Spring context, chính làFilterChainProxy.FilterChainProxychọnSecurityFilterChainđầu tiên khớp request rồi chạy các security filter trong đó, và dọn SecurityContext sau request.
🇬🇧 English:
DelegatingFilterProxyis a standard Servlet filter registered with the container. It delegates to a Spring bean namedspringSecurityFilterChain, which is theFilterChainProxy. TheFilterChainProxypicks the firstSecurityFilterChainthat matches the request, runs its security filters, and clears the security context afterwards.
Q3. Một ứng dụng có nhiều SecurityFilterChain thì chạy thế nào?
🇻🇳 Tiếng Việt:
Mỗi chain có
securityMatchervà@Order. Với mỗi request chỉ chain đầu tiên khớp được dùng. Ví dụ/api/**stateless với JWT, phần còn lại dùng form login và session. Chain bắt tất cả phải đặt cuối.
🇬🇧 English:
Each chain has a
securityMatcherand an@Order. For each request, only the first matching chain is applied. For example,/api/**can be stateless with JWT while the rest of the app uses form login and sessions. The catch-all chain must have the lowest priority.
Q4. Vì sao Spring Security 6 bỏ WebSecurityConfigurerAdapter?
🇻🇳 Tiếng Việt:
Để chuyển sang cấu hình dựa trên bean: khai báo
SecurityFilterChain,UserDetailsService,PasswordEncoderlà các@Beanđộc lập. Cách này rõ ràng hơn, dễ có nhiều chain, không cần kế thừa và override.
🇬🇧 English:
It moved to component-based configuration: we declare
SecurityFilterChain,UserDetailsServiceandPasswordEncoderas independent beans. This is more explicit, makes multiple chains easy, and avoids inheritance and overriding.
Cách nhớ ngắn gọn
- Tomcat →
DelegatingFilterProxy→FilterChainProxy→ mộtSecurityFilterChain→ DispatcherServlet. - Một request chỉ chạy một chain: chain đầu tiên khớp.
securityMatcherchọn chain,requestMatcherschọn luật quyền.ExceptionTranslationFilterđổi lỗi thành 401/403;AuthorizationFilterđứng cuối.
Tự kiểm tra
Thành phần nào kiểm tra bảo mật trước Controller: DispatcherServlet hay SecurityFilterChain?
SecurityFilterChain. Nó chạy trong Servlet Filter, đứng trước DispatcherServlet.
Vì sao cần DelegatingFilterProxy thay vì đăng ký thẳng filter với Tomcat?
Tomcat khởi tạo filter trước khi Spring context sẵn sàng. Proxy cho phép filter thật là Spring bean, được lấy lười khi có request.
Có hai chain, chain không có securityMatcher đặt @Order(1). Chuyện gì xảy ra?
Nó khớp mọi request nên chain thứ hai không bao giờ được dùng.
Filter tự viết đánh @Component và addFilterBefore thì bị gì?
Chạy hai lần: Spring Boot đăng ký nó vào Tomcat, và nó còn nằm trong security chain.
Filter nào đổi AuthenticationException/AccessDeniedException thành 401/403?
ExceptionTranslationFilter.