+2

Part 1: Các Filter trong Spring hoạt động như thế nào, ứng dụng để triển khai JWT authorization (behind the scene)

Chào ACE mình,

Mới bắt đầu học Spring Boot quả là một cái gì đó đôi lúc khiến ace phải chửi tục vì nó 😆, thật lúc đầu mình học Spring Boot để làm dự án trong team thật sự hơi bị khó chịu, à mà cái xì style của mình hơi Chí Phèo, nên ace có thể nhận thấy trong văn phong các bài viết của mình, cách hành văn nó có thể không theo khuôn mẫu nào cả, không theo một chuẩn mực xã hội nào hết, nên ae thông cảm 😆😆

Mở bài cái nào

Mình đoán trước khi đến với bài viết này có thể ace đã đọc qua một số bài viết về Filter trong Spring Boot hay cách triển khai JWT trong Spring Boot, trước mình hay gg với từ khóa Implementing JWT Authentication in a Spring Boot application Nhưng lúc đó mình kiểu 🙃 what the hell is thisss?? sao nó implement cả chục kiểu vậy chời thật sự là rối vãi chưởng.

Cóp dán mà nó chạy được là mừng lắm rồi, mà cái tính của mình là không muốn cóp y chang, muốn vừa cóp vừa custom luôn, cay cái là nó hk chạy ấy chứ 🥲 do mình cóp thiếu vì nó nhiều quá, mình nghĩ là mình sẽ custom nó lại được, nên cóp một số cái thôi kết quả là,... thôi đọc bài viết này thôi 😄

Okay chém gió mở bài nhiêu đó thôi bây giờ ace mình đi vào bài viết nhé, bài viết này gồm 2 phần chính:

  1. Bàn về Filters trong Spring nó làm tui lu mờ như thế nào

  2. Ứng dụng vào triển khai JWT middleware (gọi middleware cho nó thân thuộc với ace đi từ Node.js qua, cho nó common với mọi người 😉)


Nói nhanh về Filters trong Spring cái

Khái niệm:

Filters trong Spring là một thành phần xử lý được sử dụng để can thiệp vào quá trình xử lý request/response trong ứng dụng web. Các filters cho phép bạn thực hiện các thao tác trước hoặc sau khi một request được xử lý bởi các controller (ở đây là serverlet container) hoặc tài nguyên (resource) khác.

Filters trong Spring được triển khai dựa trên Java Servlet API và được tích hợp sâu vào hệ sinh thái của Spring Boot và Spring MVC.

image.png

Request từ client gọi đến Spring application của mình thì đầu tiên nó sẽ gặp Filters rồi nó lọc 77 49 cái lọc để quyết định xem thằng này có được đi tiếp hay không.

Để ý cái Filters thôi nhé, phần sau mình bàn ở nơi khác, ae mình chỉ cần biết là Filters (bộ lọc) nó là cái đứng trước Spring app, nó cũng như là middleware trong các framework khác ấy.

Ở trên hình minh họa thì chỉ có 2 cái bộ lọc thôi, nhưng trong thực tế nó có cả chục cái ấy, mà trong số đó 80% là các filters mặc định của Spring rồi, của mình triển khai tầm vài ba cái thôi.

Một cái nữa là Filters chain hay các filters được sắp xếp theo thứ tự, mỗi filter có thể chuyển request sang filter tiếp theo thông qua FilterChain

Okay đại khái thì mình cứ nắm Filters nó nó là Middlewares vậy thôi.

Filters trong Spring

Trong quá trình tìm hiểu về Filters trong Spring Boot thì chắc chắn mọi người cũng gặp ít nhất 1 trong 3 cái keyword này: OncePerRequestFilter, FilterGenericFilterBean.

Thì Spring cung cấp 3 loại Filter chính:

  • Filter
  • GenericFilterBean
  • OncePerRequestFilter

Okay ae mình sẽ đi tìm hiểu xem nó là gì nhé, và khi nào nên sử dụng cái nào (cái này là cái quan trọng nhất)

Bắt đầu thôi nào...

Giờ mình đi qua một số khái niệm, keywords mà ae sẽ bắt gặp trong code mẫu khi ae mới bắt đầu cài đặt Filter cho Spring app của mình.

1. Filter

Đây là interface gốc được định nghĩa trong Java Servlet API, được thiết kế để xử lý các request, response trước khi chúng đến Servlet hoặc sau khi Servlet xử lý xong.

Ace có thể hiểu đơn giản Servlet là một thành phần phía server trong Java, được sử dụng để xử lý các request từ client (thường là HTTP request) và trả về response tương ứng. Servlet là một phần của Java Servlet API và thường chạy bên trong một Servlet Container (ví dụ: Tomcat, Jetty,...).

Rãnh mình sẽ lên bài tiếp về Servlet cho ae không bắt đầu từ Servlet.

Vào coi code của nó thôi nè, đây là code định nghĩa Filter:

public interface Filter {

    default void init(FilterConfig filterConfig) throws ServletException {
    }
    
    void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException;
            
    default void destroy() {
    }
}

Vậy thì theo cài đặt ở trên, Filter là một interface nên thằng nào mà implements Filter thì phải implement các phương thức định nghĩa bên trên, sau đây mình tóm tắc mục đích của các phương thức trên:

  1. Hàm init thì gọi đầu tiên và một lần duy nhất khi Filter được khởi tạo

  2. Hàm doFilter

    Đây là phương thức quan trọng nhất, được container(ở đây là Servlet container đó) gọi mỗi khi có yêu cầu HTTP đi qua Filter. Trong phương thức này:

    • Xử lý request: Kiểm tra hoặc thay đổi request trước khi chuyển tiếp đến Servlet hoặc Filter tiếp theo trong chuỗi.
    • Chuyển tiếp request: Gọi chain.doFilter(request, response) để chuyển request đến Filter tiếp theo hoặc Servlet.
    • Xử lý response: Có thể thay đổi nội dung hoặc header của response trước khi gửi về client.
    • Có thể dừng chuỗi xử lý (filterChain) nếu Filter quyết định không gọi chain.doFilter().
  3. Hàm destroy

    • Được container gọi khi Filter bị loại bỏ (ứng dụng dừng hoặc Filter không còn được sử dụng).
    • Dùng để giải phóng tài nguyên hoặc thực hiện các thao tác dọn dẹp như đóng kết nối cơ sở dữ liệu, file, v.v.

Do thằng Filter là interface gốc của Java Servlet API nên nếu triển khai filter từ thằng này, mình phải implement các phương thức của nó, ví dụ:

@Component
public class StandardFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) {
        System.out.println("Filter initialized");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("Processing request...");
        chain.doFilter(request, response); // Chuyển sang filter tiếp theo
    }

    @Override
    public void destroy() {
        System.out.println("Filter destroyed");
    }
}

2. GenericFilterBean

  • Đây là lớp tiện ích được Spring cung cấp, giúp giảm boilerplate code khi làm việc với filters.
  • GenericFilterBean đã cung cấp các phương thức init()destroy().
public abstract class GenericFilterBean implements Filter, BeanNameAware, EnvironmentAware, EnvironmentCapable, ServletContextAware, InitializingBean, DisposableBean {
    ...
	@Override
	public void destroy() {
	}
    
    @Override
	public final void init(FilterConfig filterConfig) throws ServletException {
        ...
    }
    ...
}

Phương thức doFilter() không được triển khai trong GenericFilterBean. Vì vậy, mọi lớp con kế thừa GenericFilterBean đều bắt buộc phải override phương thức này để định nghĩa logic xử lý request và response.

Do đó khi sử dụng ae chỉ cần kế thừa GenericFilterBean và override phương thức doFilter() để thực hiện logic chính. Ví dụ:

@Component
public class MyFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        System.out.println("GenericFilterBean in action");
        chain.doFilter(request, response);
    }
}

3. OncePerRequestFilter

OncePerRequestFilter là một lớp trừu tượng (abstract class) do Spring cung cấp, kế thừa từ GenericFilterBean. Vì vậy:

  • Các phương thức như init()destroy() đã được GenericFilterBean triển khai sẵn, mọi người không cần phải viết lại.
  • Lớp này đã implement phương thức doFilter()GenericFilterBean không triển khai nó.
  • Và lớp định nghĩa thêm một phương thức doFilterInternal() mà khi mọi người extends từ OncePerRequestFilter thì phải implement nó để xử lý các logic của mình thay vì doFilter() như 2 thằng kia.

Mục đích của OncePerRequestFilter

OncePerRequestFilter được thiết kế để đảm bảo một filter chỉ được thực thi một lần duy nhất cho mỗi request. Điều này đặc biệt hữu ích trong các tình huống mà request có thể được xử lý lại bởi các dispatcher khác nhau, ví dụ như FORWARD, INCLUDE, hay ERROR.

Phân tích doFilter() trong OncePerRequestFilter

Để có thể được gọi là OncePerRequestFiltern thì phương thức doFilter đã được lớp OncePerRequestFilter override lại như thế nào. Dưới đây là code triển khài của doFilter() trong OncePerRequestFilter.

Ae có thể thấy là Spring Boot họ đã xử lý logic để Filter chạy một lần bằng cách dùng một cờ đánh dấu request.

@Override
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

    // Kiểm tra request/response có phải là HTTP không
    if (!((request instanceof HttpServletRequest httpRequest) && (response instanceof HttpServletResponse httpResponse))) {
        throw new ServletException("OncePerRequestFilter only supports HTTP requests");
    }

    // Tên attribute để đánh dấu request đã được xử lý qua filter này
    String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();
    boolean hasAlreadyFilteredAttribute = request.getAttribute(alreadyFilteredAttributeName) != null;

    // Nếu cần bỏ qua filter trong một số trường hợp (do developer định nghĩa)
    if (skipDispatch(httpRequest) || shouldNotFilter(httpRequest)) {
        filterChain.doFilter(request, response); // Chuyển tiếp request mà không thực hiện logic filter
    }
    else if (hasAlreadyFilteredAttribute) {
        // Nếu filter đã được thực thi trước đó
        if (DispatcherType.ERROR.equals(request.getDispatcherType())) {
            doFilterNestedErrorDispatch(httpRequest, httpResponse, filterChain);
            return;
        }
        filterChain.doFilter(request, response); // Chuyển tiếp request
    }
    else {
        // Gán attribute để đánh dấu filter đã được xử lý
        request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);
        try {
            // Thực hiện logic filter chính (do developer định nghĩa)
            doFilterInternal(httpRequest, httpResponse, filterChain);
        }
        finally {
            // Xóa attribute đã đánh dấu
            request.removeAttribute(alreadyFilteredAttributeName);
        }
    }
}

Ví dụ

Kế thừa OncePerRequestFilter để xác thực JWT:

@Component
public class JWTAuthorizationFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        // Kiểm tra và xác thực JWT token
        String jwt = request.getHeader("Authorization");
        if (jwt == null || !isValidToken(jwt)) {
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            return;
        }

        // Chuyển tiếp request nếu xác thực thành công
        filterChain.doFilter(request, response);
    }

    private boolean isValidToken(String token) {
        // Logic kiểm tra token (ví dụ: decode, verify chữ ký, vv.)
        return true; // Giả sử token luôn hợp lệ
    }
}

Tóm lại

  • OncePerRequestFilter kế thừa GenericFilterBean và cung cấp cơ chế giúp filter chỉ chạy một lần duy nhất trên mỗi request.
  • Bạn chỉ cần override phương thức doFilterInternal() để thực hiện logic chính của mình.
  • Nó hỗ trợ nhiều tính năng tiện lợi như bỏ qua filter cho một số request (shouldNotFilter()) hoặc tránh thực thi lặp với cùng một request (alreadyFilteredAttributeName).

Tóm lại thì bài hơi dài rồi, và ace mình cũng đã đi qua các khái niệm cơ bản về Filters trong Spring, vậy nên mình chốt bài này là phần 1, mình sẽ cập nhật thêm phần 2 trong bài viết tiếp theo nhé.

Cảm ơn mọi người đã đọc đến đây, nếu ace mình thắc mắc hay chưa hiểu chỗ nào thì cứ cmt nhé, mình sẽ cố gắng giải đáp thắc mắc cho mọi người, à nếu ae mình thấy bài viết có chỗ nào chưa hợp lý thì feedback giúp mình luôn, để mình chỉnh sửa lại cho phù hợp. Thanks mọi người 🥹

Hẹn ae ở bài tiếp theo....

Hẹn ae ở phần 2 🥹

Phần 2 ở đây nha anh em


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.