Chào Mọi người,
Tiếp nối từ Part 1, tại sao nên dùng OncePerRequestFilter
để triển khai xác thực JWT?
Trong bài viết này, mình sẽ giải thích tại sao nên sử dụng OncePerRequestFilter
để triển khai xác thực JWT, thay vì dùng một lớp Filter
thông thường hoặc kế thừa từ GenericFilterBean
.
Lý do chính nằm ở chỗ OncePerRequestFilter
được thiết kế đặc biệt cho các tác vụ xử lý một lần duy nhất trong mỗi yêu cầu HTTP, đảm bảo không bị xử lý lặp lại trong các tình huống đặc thù như forward, include, hoặc error dispatch. Đây là một điểm mấu chốt để hiểu tại sao OncePerRequestFilter
là lựa chọn tối ưu khi triển khai các cơ chế xác thực JWT.
1. Tại sao không dùng Filter
?
- Filter là interface gốc trong Java Servlet API, cung cấp các phương thức cơ bản như
init
,doFilter
, vàdestroy
. Tuy nhiên, nếu tự triển khaiFilter
, bạn cần tự quản lý toàn bộ luồng xử lý, bao gồm cả logic để tránh việc xử lý lặp lại trong cùng request. - Nhược điểm:
- Dễ dẫn đến lỗi khi filter xử lý lặp lại trong các trường hợp như forward hoặc error dispatch.
- Code phức tạp hơn do bạn phải tự viết các logic kiểm tra và quản lý.
Ví dụ:
@Component
public class JwtFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // Kiểm tra và xử lý JWT // Lặp lại không kiểm soát khi có forward chain.doFilter(request, response); }
}
Hạn chế: Filter
sẽ không tự động bỏ qua các dispatch không mong muốn như forward hay error, dẫn đến xử lý JWT lặp lại.
Demo trường hợp Filter có thể chạy 2 lần
Ví dụ mình có một cái StandardFilter
được implement như sau:
@Component
public class StandardFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.err.println("StandardFilter.init"); Filter.super.init(filterConfig); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { System.err.println("StandardFilter.doFilter"); chain.doFilter(request, response); } @Override public void destroy() { System.err.println("StandardFilter.destroy"); Filter.super.destroy(); }
}
Đây là DemoController
của mình trong hàm handleRequest
mình có forward
tới api /demo/forward
anh em có thể triển khai tương tự để thấy dòng err StandardFilter.doFilter
sẽ được in ra 2 lần khi mình gọi /demo/request
.
@RestController
@RequestMapping("/demo")
public class DemoController { @GetMapping("/request") public void handleRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.getRequestDispatcher("/demo/forward").forward(request, response); } @GetMapping("/forward") public String handleForward() { return "Forwarded Response"; }
}
Vậy thì nếu tui cố chấp dùng Filter
có được không, câu trả lời là vẫn được, sau đây là code demo sử dụng Filter
để xác thực JWT cho request:
Demo JWT Authentication dùng Filter
Class JWTAuthFilter
được cài đặt như sau:
@Component
public class JWTAuthFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { System.out.println("JWTAuthFilter initialized"); } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest httpRequest) { System.err.println("Filter executed for: " + httpRequest.getRequestURI() + " " + httpRequest.getMethod()); System.err.println("DispatcherType: " + httpRequest.getDispatcherType()); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; System.err.println("JWTAuthFilter invoked"); // Extract Authorization header String authHeader = httpRequest.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("Unauthorized"); return; } String token = authHeader.substring(7); // Remove "Bearer " prefix try { // Validate the JWT token (pseudo-code, replace with your logic) String username = validateAndExtractUsername(token); // Set authentication in the SecurityContext if valid Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); System.out.println("Authentication: " + (authentication != null ? authentication.getName() : "null")); SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(username, null, List.of()) ); } catch (Exception e) { httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("Invalid Token"); return; } // Continue to the next filter chain.doFilter(request, response); } @Override public void destroy() { System.out.println("JWTAuthFilter destroyed"); } private String validateAndExtractUsername(String token) throws Exception { // Pseudo-logic for token validation if ("validToken".equals(token)) { return "user123"; // Extract username from the token } throw new Exception("Invalid Token"); }
}
Class WebSecurityConfig
được cài đặt như sau:
@Configuration
public class WebSecurityConfig { @Autowired JWTAuthFilter jwtAuthFilter; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authorizeRequests) -> { authorizeRequests.requestMatchers("/api/**").authenticated().anyRequest().permitAll(); }); http.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class); http.exceptionHandling(exception -> exception.authenticationEntryPoint((request, response, authException) -> { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("Custom 401: Authentication required!"); }).accessDeniedHandler((request, response, accessDeniedException) -> { response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.getWriter().write("Custom 403: Forbidden access!"); })); return http.build(); }
}
Anh em để ý System.err.println("JWTAuthFilter invoked");
nó sẽ output 2 lần:
Authentication: null 2024-12-01T21:38:36.424+07:00 WARN 33854 --- [nio-8080-exec-1] o.s.w.s.h.HandlerMappingIntrospector : Cache miss for REQUEST dispatch to '/api/users' (previous null). Performing MatchableHandlerMapping lookup. This is logged once only at WARN level, and every time at TRACE.
2024-12-01T21:38:36.425+07:00 DEBUG 33854 --- [nio-8080-exec-1] o.s.security.web.FilterChainProxy : Secured GET /api/users Authentication: user123 Filter executed for: /api/users GET
DispatcherType: REQUEST
JWTAuthFilter invoked Filter executed for: /api/users GET
DispatcherType: REQUEST
JWTAuthFilter invoked
Tóm tắt lý do filter được gọi lại
Code trên, bạn sẽ thấy thông tin Authentication
trước và sau khi thiết lập nó trong SecurityContextHolder
. Điều này sẽ giúp bạn hiểu rõ hơn về sự thay đổi context và lý do tại sao filter được gọi lại.
- Lần đầu tiên,
SecurityContextHolder.getContext().getAuthentication()
trả vềnull
, vì chưa có authentication. - Sau khi bạn thiết lập authentication vào
SecurityContextHolder
, Spring Security sẽ đánh giá lại security context và có thể kích hoạt lại các filter trong filter chain. - Điều này có thể gây ra việc filter được gọi lại và
SecurityContextHolder.getContext().getAuthentication()
lúc này có giá trị xác thực (vì bạn đã thiết lậpUsernamePasswordAuthenticationToken
).
Kết luận:
Anh em cố đấm ăn xôi thì cũng dùng Filter
để implement JWT cũng được nhưng không ai khuyến khích điều đó vì anh em phải handle cả đóng thứ, đầu tiên 3 thằng init()
, doFilter()
, destroy()
chưa kể anh em xử lý các case mà Filter chỉ chạy một lần nữa 🥲
2. Tại sao không dùng GenericFilterBean
?
- GenericFilterBean là một abstract class được Spring cung cấp để đơn giản hóa việc triển khai
Filter
. Nó giúp bạn bỏ qua việc tự triển khai các phương thứcinit
vàdestroy
. - Tuy nhiên,
GenericFilterBean
không cung cấp cơ chế đảm bảo filter chỉ được thực thi một lần trong mỗi request. Nếu bạn cần xử lý JWT một lần duy nhất trong toàn bộ lifecycle của request, bạn phải tự viết thêm logic kiểm tra, tương tự như khi dùngFilter
.
Demo dùng GenericFilterBean
Nó cũng y chang thằng kia thôi, được cái chỉ cần implement doFilter()
2 thằng kia thì thằng GenericFilterBean
nó implement cho mình rồi.
@Component
public class JWTAuthFilter extends GenericFilterBean { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { if (request instanceof HttpServletRequest httpRequest) { System.err.println("Filter executed for: " + httpRequest.getRequestURI() + " " + httpRequest.getMethod()); System.err.println("DispatcherType: " + httpRequest.getDispatcherType()); } HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; System.err.println("JWTAuthFilter invoked"); // Extract Authorization header String authHeader = httpRequest.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("Unauthorized"); return; } String token = authHeader.substring(7); // Remove "Bearer " prefix try { // Validate the JWT token (pseudo-code, replace with your logic) String username = validateAndExtractUsername(token); // Set authentication in the SecurityContext if valid SecurityContextHolder.getContext().setAuthentication( new UsernamePasswordAuthenticationToken(username, null, List.of()) ); } catch (Exception e) { httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); httpResponse.getWriter().write("Invalid Token"); return; } // Continue to the next filter chain.doFilter(request, response); } private String validateAndExtractUsername(String token) throws Exception { // Pseudo-logic for token validation if ("validToken".equals(token)) { return "user123"; // Extract username from the token } throw new Exception("Invalid Token"); }
}
Kết luận
Nói chung thì nó cũng không đảm bảo Filter chạy 1 lần, để debug cũng bù đầu mốc mắt ấy, bạn nào có hứng thú thì cứ comment, mình rãnh mình đi sâu vào phần này 🤪🤪
3. Tại sao nên dùng OncePerRequestFilter
?
Cái này mục tiêu chính của bài nên đi vô cái này thôi nè, còn thằng GenericFilterBean
do mình làm biếng viết tiếp ấy, nào rãnh đào sâu vào nó tiếp
a. Xử lý một lần duy nhất cho mỗi request
OncePerRequestFilter
đảm bảo logic của bạn chỉ được thực thi một lần duy nhất trong lifecycle của request, ngay cả khi request đó trải qua các giai đoạn như forward, include, hay error dispatch.- Điều này đặc biệt quan trọng trong việc xác thực JWT, vì bạn không muốn kiểm tra token lại khi request được forward hoặc xử lý lỗi.
Cách hoạt động:
OncePerRequestFilter
sử dụng một cơ chế gắn cờ (flag) bằng cách thêm một thuộc tính vàoHttpServletRequest
để đánh dấu rằng filter đã được thực thi.- Nếu cờ này đã được gắn, filter sẽ bỏ qua và chuyển tiếp đến bước tiếp theo trong chuỗi xử lý.
b. Cung cấp phương thức doFilterInternal
OncePerRequestFilter
yêu cầu anh em override phương thứcdoFilterInternal()
, còn lại thìOncePerRequestFilter
nó làm hết cho mình rồi, anh em thích vọc vạch thì nhảy vô code ở hàmdoFilter()
của nó xem cách nó handle để đảm bảo filter chỉ chạy 1 lần như thế nào, như nói ở trên thì nó có 1 cái flag đánh dấu request thôi.
Demo JWT dùng OncePerRequestFilter
Đây là cài đặt class JWTAuthorizationFilter
anh em thấy mình extends từ OncePerRequestFilter
thì mình chỉ cần override doFilterInternal()
là được.
@Component
public class JWTAuthorizationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String header = request.getHeader("Authorization"); if (header == null || !header.startsWith("Bearer ")) { System.err.println("No jwt fond in request header"); return; } String token = header.substring(7); String email = JWTProvider.validateAndParseToken(token); if (email.isEmpty()) { System.err.println("Invalid token"); return; } CustomAuthentication authentication = new CustomAuthentication(email, true); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception ex) { System.err.println(ex); } finally { filterChain.doFilter(request, response); } }
}
Anh em để ý dòng này
CustomAuthentication authentication = new CustomAuthentication(email, true);
Tui có custom một cái Authentication không dùng default như code mẫu ở trên (đơn giản lần này tui không thích thôi, lý do phức tạp thì bài viết khác 😂)
Chỗ
SecurityContextHolder.getContext().setAuthentication(authentication);
nó nhận vào một thằngAuthentication
thì anh em cứ set cái gì mà nó implements cáiinterface Authentication
là được:
Vậy nên anh em không muốn dùng thằng UsernamePasswordAuthenticationToken
thì anh anh cứ custom lại một cái CustomAuthentication
của anh em là được nó chỉ cần implements thằng Authentication
là được (này rãnh thì chắc cũng lên bài tiếp 😄)
Thì đây là code cài đặt CustomAuthentication
:
public class CustomAuthentication implements Authentication { private final String principal; // User identity, e.g., username or ID private boolean authenticated; public CustomAuthentication(String principal, boolean authenticated) { this.principal = principal; this.authenticated = authenticated; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; // Return authorities/roles if needed } @Override public Object getCredentials() { return null; // Return credentials if applicable } @Override public Object getDetails() { return null; // Return additional details if applicable } @Override public Object getPrincipal() { return principal; // Return the principal } @Override public boolean isAuthenticated() { return authenticated; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { this.authenticated = isAuthenticated; } @Override public String getName() { return principal; // Return a name or identifier for the principal }
}
Rồi thì trên đây là code mình cài đặt JWT dùng OncePerRequestFilter
vậy đó, anh em thử cài đặt xem có chạy được hông, hông được thì tự debug đi 😂 😂, tui đoán là lần chắc trật vuột mấy lần ấy chứ (Có khi anh em chửi thầm móa thằng này đưa code gì cóp dán vô méo chạy 😆)
Vậy khi nào không dùng OncePerRequestFilter
?
- Khi không cần đảm bảo xử lý một lần duy nhất cho mỗi request (ví dụ: xử lý cache hoặc thêm header cho từng forward).
- Khi không làm việc với Spring Security hoặc không cần xác thực JWT.
Kết luận
- Dùng
OncePerRequestFilter
trong Spring Boot là lựa chọn tối ưu để implement JWT vì:- Đảm bảo xử lý một lần duy nhất cho mỗi yêu cầu HTTP.
- Đơn giản hóa logic xử lý với phương thức
doFilterInternal
.
Thanks
Thôi bài viết nhiêu đó thôi nhe anh em, phân tích một chút lý do tại sao không nên dùng Filter
, còn anh em xem tut hướng dẫn implement JWT authentication thì họ dùng OncePerRequestFilter
thì hiểu lý do rồi đó.
Anh em muốn mình đi sâu hơn phần nào thì cứ comment (để mình cũng có cơ hội tìm hiểu kỹ hơn luôn) với mình rãnh thì mình cũng edit lại một số chỗ giải thích thêm một số cái 🥹🥹
Rồi cảm ơn anh chị em mình đã đọc bài, muốn feedback gì thì cứ thoải mái feedback đi cho lên tương tác đồ 😁