Spring boot Filter 機制,攔截請求與回應

Spring boot Filter 機制,攔截請求與回應

2021, Nov 22    

這次要介紹的 Filter 是用來針對 Http 的請求與回應在途中攔截下來做一些處理,運作邏輯上有一點像先前寫過的 AOP,但 AOP 是以 Method 為視角去攔截,而 Filter 則是以 Servlet 的層級來攔截,應用的場景會稍有不同

Filter

首先來撰寫一個簡單的 Filter 將收到的請求以及回應的各種資訊記錄下來

@Slf4j
public class OnceFilter extends OncePerRequestFilter {


    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        final String queryString =
            req.getQueryString() == null ? "" : "?" + req.getQueryString();

        log.info("[Request] {} {}{}", req.getMethod(), req.getRequestURI(), queryString);
        chain.doFilter(req, res);
        log.info("[Response] Status: {}", res.getStatus());
    }
}

一般的 Filter 其實也可以透過 Implements Filter 來實作,而 Spring 多提供了一個 OncePerRequestFilter 用以確保每個 Request 只會經過一次

而一個請求在多個 Filter 間串接起來就被稱為 FilterChainchain.doFilter 就是指往下一個 Filter 前進,並且在那之後才收到 Response 的物件,在 chain.doFilter 執行之前 Response 是拿不到東西的

註冊

將上面的 Filter 完成後應該會發現實際上還沒有作用,Spring boot 的物件基本上都需要通過註冊成為 Bean 的過程來啟用,Filter 也不例外,下面介紹三種 Filter 註冊的方式

FilterRegistrationBean

首先是比較正規透過 Configuration 的方式來註冊完整的 Filter 屬性

@Configuration
public class AppConfig {
    @Bean
    public FilterRegistrationBean filterRegistration() {
        FilterRegistrationBean<OnceFilter> bean = new FilterRegistrationBean<>();
        bean.setFilter(new OnceFilter());
        bean.addUrlPatterns("/*");
        bean.setOrder(1);
        bean.setName("onceFilter");
        bean.setEnabled(true);
        return bean;
    }
}

可以針對需要的 Url 來 Filter,如果有多個 Filter 也可以指定順序(數字越小越優先)以及啟用與否

WebFilter

不想多寫一個 Configuration 來註冊的話可以透過 @WebFilter 來取代,一樣可以指定 Url,執行順序則可以透過 @Order 來給定,只不過 @WebFilter 不是由 Spring 提供的,因此預設不會被掃描到,必須在進入點的主類別額外加上 @ServletComponentScan 才可以使用

@SpringBootApplication()
@ServletComponentScan()
public class Application {
    SpringApplication.run(Application.class, args);
}

@WebFilter(urlPatterns = "/*", filterName = "onceFilter")
@Order(1)
@Slf4j
public class OnceFilter extends OncePerRequestFilter {
    ...
}

Component

如果還是嫌棄上述的作法太麻煩,最簡單的作法可以直接加上 @Component 直接將物件在 Spring boot 啟用時註冊成 Bean,預設上的 Url 就是全部,如果需要排序也可以加上 @Order,實際上沒有什麼特殊用途的話應該是蠻堪用了

@Component
@Order(1)
@Slf4j
public class OnceFilter extends OncePerRequestFilter {
    ...
}

Exclude

有時候不是想要設定需要的 URL,而是要設定不需要的 URL,這時候就要去 Override Filter 的 shouldNotFilter

@Slf4j
public class OnceFilter extends OncePerRequestFilter {

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        String path = request.getRequestURI();
        return "/health".equals(path);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest req,
                                    HttpServletResponse res,
                                    FilterChain chain) throws ServletException, IOException {
        final String queryString =
            req.getQueryString() == null ? "" : "?" + req.getQueryString();

        log.info("[Request] {} {}{}", req.getMethod(), req.getRequestURI(), queryString);
        chain.doFilter(req, res);
        log.info("[Response] Status: {}", res.getStatus());
    }
}

只要 shouldNotFilterreturntrue 就不會經過這個 Filter,其實也可以用於其他判斷,可能對於不同的權限、地區限制不同的 URL 之類的