Part 2: Tại sao không nên dùng Filter hay GenericFilterBean trong JWT authentication, và nên dùng OncePerRequestFilter

0 0 0

Người đăng: Sang Le

Theo Viblo Asia

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 khai Filter, 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.

  1. Lần đầu tiên, SecurityContextHolder.getContext().getAuthentication() trả về null, vì chưa có authentication.
  2. 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.
  3. Đ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ập UsernamePasswordAuthenticationToken).

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ức initdestroy.
  • 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ùng Filter.

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ào HttpServletRequest để đá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ức doFilterInternal(), 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àm doFilter() 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ằng Authentication thì anh em cứ set cái gì mà nó implements cái interface 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 đồ 😁

Bình luận

Bài viết tương tự

- vừa được xem lúc

How JSON Web Token(JWT) authentication works?

1. JWT (JSON Web Token) là gì . Thông tin này có thể xác minh và đáng tin cậy vì nó là chữ ký điện tử . Jwt có thể được đăng ký băng cách sử dụng bí mật (với thuật toán HMAC) hoặc cặp khóa public/private bằng RSA.

0 0 71

- vừa được xem lúc

JWT và ứng dụng xác thực người dùng trong Rails

JWT. Thời gian gần đây mình có init API thì mình có ứng dụng Json Web Token (JWT) để xây dựng xác thực người dùng. Nó có support những gì và ứng dụng của nó ra sao thì mình xin chia sẻ trong bài viết. Nó là gì.

0 0 175

- vừa được xem lúc

Tìm hiểu một chút về JWT

Hello mọi người, trong bài viết hôm nay mình cùng các bạn sẽ cùng tìm hiểu về JWT nhé. JWT ( Json Web Token ) là 1 tiêu chuẩn mở (RFC 7519) định nghĩa cách truyền thông tin một cách an toàn giữa các b

0 0 73

- vừa được xem lúc

Refresh token là gì? Cách hoạt động có khác gì so với token không?

Ở những bài trước chúng ta đã nói nhiều về JWT là gì? Vì sao phải sử dụng JWT, trong suốt quá trình sử dụng JWT chúng tôi có nhận được nhiều phản hồi về chủ đề JWT. Trong đó có một vấn đề mà có nhiều

0 0 101

- vừa được xem lúc

JWT và ứng dụng của nó

Khái niệm. JSON Web Token (JWT) là 1 tiêu chuẩn mở (RFC 7519) định nghĩa cách thức truyền tin an toàn giữa bên bằng 1 đối tượng JSON.

0 0 45

- vừa được xem lúc

JWT - Từ cơ bản đến chi tiết

Chào mọi người. Bài viết của mình ngày hôm nay với mục đích chia sẻ những kiến thức mà mình lượm nhặt được, gom chúng lại để tổng hợp cho các bạn.

0 0 45