spring boot slf4j logback listener filter json aop http get scheduling tasks

Spring Boot EP 11:實作Filter處理Request與Response

Filter

有些事情希望在Request進入Controller之前或Response後且回覆客戶端以前做,並且這些事情還需要按照順序執行,這就是Filter的功能,實際使用上,可以同時存在許多個Filter,透過排序,讓Filter的執行向Pipeline一樣,其順序如下圖:

除了一般的Filter外 (Spring官方說明),還有另一個稱為OncePerRequestFilter的Filter,由名稱可以得知OncePerRequestFilter在單一個Request只執行一次,而且獲得系統保證,不會重複執行,這非常適合用來處理驗證…等需求。

接下來要來實作Filter,案例為:Request -> 檢查驗證 -> 檢查Request格式 -> 紀錄Request/Response。

以上的動作分別以不同的Filter來實作,如下:

  • OncePerRequestFilter:檢查驗證
  • Filter:檢查Request格式、紀錄Request/Response

實作OncePerRequestFilter用於檢查驗證

步驟1:在”STOCKMARKET/src/main/java/stockmarket/jovepater/com/stockmarket/”下建立一個名為Filters的資料夾,作為放置Filter的地方,並新增一個名為AuthenticationFilter.java的檔案。

步驟2:實作驗證功能,因實際情況,只驗證是否有Bearer字串透過Header傳進來,有的話即算驗證成功。

package stockmarket.jovepater.com.stockmarket.Filters;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import stockmarket.jovepater.com.stockmarket.Classes.RspBody;

// 套用到全專案
@Component
// 指定這個Filter的執行順序,順序由小至大,越小表示越先執行
@Order(0)
// 拓展OncePerRequestFilter
public class AuthenticationFilter extends OncePerRequestFilter {

    // 宣告Logger
    private static final Logger myLogger = LoggerFactory.getLogger(HttpTransatcionLoggingFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        String strRequestURI = null;

        // 因為OncePerRequestFilter僅適用HTTP Request,所以不需要另外宣告HttpServletRequest
        // 取得Request中名為Authorization的Header內容
        String strToken = Objects.toString(request.getHeader("Authorization"), "");
        strRequestURI = request.getRequestURI();
        // 檢查名為Authorization的Header內容是否帶有Bearer的字串
        if (strToken.contains("Bearer") == true) {
            myLogger.info("Request: {} {} , Msg: {}", request.getMethod(), strRequestURI, "Auth OK.");

            // 表示繼續執行下一個Filter,並且把Reques/Response傳下去
            filterChain.doFilter(request, response);
        } else {
            // 設定Response編碼為UTF-8
            response.setCharacterEncoding("UTF-8");
            // 設定Response的ContentType
            response.setContentType("application/json; charset=utf-8");
            // 驗證失敗,設定Response的狀態為401
            response.setStatus(401);

            // 將Object轉換成JSON字串
            ObjectMapper objectMapper = new ObjectMapper();
            String jsonRspBody = objectMapper.writeValueAsString(new RspBody("0401", "unauth", "Auth Fail."));

            // 宣告PrintWriter來回傳內容
            PrintWriter printWriter = response.getWriter();
            printWriter.append(jsonRspBody);
            printWriter.close();
            myLogger.info("Request: {} {} , Msg: {}", "401", strRequestURI, "Auth Fail.");
            // 沒有filterChain.doFilter被執行,所以系統只會執行至此,然後回覆給客戶端401的結果
        }
    }
}

步驟3:當呼叫任何一支API時,若沒有在Header中放入Authorization及以Bearer為開頭的Token,日誌會出現驗證失敗的訊息,並回覆401狀態給客戶端。

驗證失敗的401訊息如下:

2022-02-03 14:58:29.349 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Filters.AuthenticationFilter: Request: 401 /api/hello , Msg: Auth Fail.

步驟4:若Header中放入Authorization及以Bearer為開頭的Token (此範例中Token為隨意的字串),客戶端就可以正常通過驗證,如下圖:

並且會在日誌中看見驗證成功的訊息:

2022-02-03 14:59:36.657 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Filters.AuthenticationFilter: Request: GET /api/hello , Msg: Auth OK.

實作一般Filter用於檢查Request格式與紀錄Request/Response

一個Request通過驗證後,接著進行格式檢查與後續的日誌記錄,實作一般的Filter來達到目的,雖說可以寫在同一個Filter中,但Filter本身具有Pipeline的特性,只要設定@Order(int)即可按順序執行,再依照不同功能拆成個個小模組,更有利於後續維護。

步驟1:在Filters資料夾中建立名為RequestCheckingFilter.java,實現Request格式檢查,在此我們僅檢查ContentType。

package stockmarket.jovepater.com.stockmarket.Filters;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import stockmarket.jovepater.com.stockmarket.Classes.RspBody;

@Component
// 順序設為1,在Auth的0順序後面執行
@Order(1)
public class RequestCheckingFilter implements Filter {

    private static final Logger myLogger = LoggerFactory.getLogger(HttpTransatcionLoggingFilter.class);

    // init方法是Filter的預設方法,其用於該Filter最一開始的動作,若沒有特別的需求,可以不實作任何動作
    @Override
    public void init(FilterConfig filterconfig) throws ServletException {
    }

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

        // 宣告Request/Response為HttpServletRequest/HttpServletResponse物件,將HTTP的內容導入
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 透過httpRequest.getContentType()取得ContentType的內容
        String strContentType = Objects.toString(httpRequest.getContentType(), "");

        String strRequestURI = httpRequest.getRequestURI();

        // 若ContentType是application/json,則通過,否則回覆422的錯誤狀態
        if (strContentType.equals("application/json") == true) {
            myLogger.info("Request: {} {} , Msg: {}", httpRequest.getMethod(), strRequestURI,
                    "Content Type Checked.");

            // filterChain.doFilter方法表示執行下一個Filter
            filterChain.doFilter(request, response);
        } else {
            httpResponse.setCharacterEncoding("UTF-8");
            httpResponse.setContentType("application/json; charset=utf-8");
            // 回覆的HTTP狀態指定為422
            httpResponse.setStatus(422);

            ObjectMapper objectMapper = new ObjectMapper();
            String jsonRspBody = objectMapper.writeValueAsString(new RspBody("0422", "Wrong Content Type",
                    "The server understands the content type of the request entity, but it's not right type for it."));

            PrintWriter printWriter = response.getWriter();
            printWriter.append(jsonRspBody);
            printWriter.close();
            myLogger.info("Request: {} {} , Msg: {}", "422", strRequestURI, "Auth Fail.");
        }

    }

    // destroy是Filter的預設方法,其表示Filter結束時觸發的動作,如init一樣,可以不用特別去實作
    @Override
    public void destroy() {
    }
}

步驟2:在Filters資料夾中建立名為HttpTransatcionLoggingFilter.java,實作將Request/Response印在日誌上,這個技巧通常用作紀錄Request/Response作為System Log。

ppackage stockmarket.jovepater.com.stockmarket.Filters;

import java.io.IOException;
import java.io.PrintWriter;
import java.util.Objects;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.fasterxml.jackson.databind.ObjectMapper;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import stockmarket.jovepater.com.stockmarket.Classes.RspBody;

@Component
// 順序設為1,在Auth的0順序後面執行
@Order(1)
public class RequestCheckingFilter implements Filter {

    private static final Logger myLogger = LoggerFactory.getLogger(HttpTransatcionLoggingFilter.class);

    // init方法是Filter的預設方法,其用於該Filter最一開始的動作,若沒有特別的需求,可以不實作任何動作
    @Override
    public void init(FilterConfig filterconfig) throws ServletException {
    }

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

        // 宣告Request/Response為HttpServletRequest/HttpServletResponse物件,將HTTP的內容導入
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 透過httpRequest.getContentType()取得ContentType的內容
        String strContentType = Objects.toString(httpRequest.getContentType(), "");

        String strRequestURI = httpRequest.getRequestURI();

        // 若ContentType是application/json,則通過,否則回覆422的錯誤狀態
        if (strContentType.equals("application/json") == true) {
            myLogger.info("Request: {} {} , Msg: {}", httpRequest.getMethod(), strRequestURI,
                    "Content Type Checked.");

            // filterChain.doFilter方法表示執行下一個Filter
            filterChain.doFilter(request, response);
        } else {
            httpResponse.setCharacterEncoding("UTF-8");
            httpResponse.setContentType("application/json; charset=utf-8");
            // 回覆的HTTP狀態指定為422
            httpResponse.setStatus(422);

            ObjectMapper objectMapper = new ObjectMapper();
            String jsonRspBody = objectMapper.writeValueAsString(new RspBody("0422", "Wrong Content Type",
                    "The server understands the content type of the request entity, but it's not right type for it."));

            PrintWriter printWriter = response.getWriter();
            printWriter.append(jsonRspBody);
            printWriter.close();
            myLogger.info("Request: {} {} , Msg: {}", "422", strRequestURI, "Wrong type.");
        }

    }

    // destroy是Filter的預設方法,其表示Filter結束時觸發的動作,如init一樣,可以不用特別去實作
    @Override
    public void destroy() {
    }
}

步驟3:透過Postman發出一個ContentType為application/xml的Header來呼叫Restful API,會獲得422狀態的錯誤訊息。

日誌的錯誤訊息如下:

# 先顯示驗證通過,證明以@Order()來控制的順序是有效的
2022-02-03 16:09:40.633 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Filters.AuthenticationFilter: Request: GET /api/hello , Msg: Auth OK.
# 因為ContentType不是規定的application/json,系統回覆422的錯誤狀態
2022-02-03 16:09:40.665 INFO  [http-nio-8080-exec-1] stockmarket.jovepater.com.stockmarket.Filters.RequestCheckingFilter: Request: 422 /api/hello , Msg: Wrong Content Type.

步驟4:將ContentType改為application/json,再次呼叫Restful API,此時就能正常運作,並且獲得200的成功狀態。

日誌訊息如下:

# 先驗證客戶端
2022-02-03 16:13:55.927 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Filters.AuthenticationFilter: Request: GET /api/hello , Msg: Auth OK.
# 再來檢查Content Type
2022-02-03 16:13:55.928 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Filters.RequestCheckingFilter: Request: GET /api/hello , Msg: Content Type Checked.
# 紀錄Request
2022-02-03 16:13:55.928 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Request: GET /api/hello , from: 127.0.0.1
2022-02-03 16:13:55.947 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: This is Hello API.
2022-02-03 16:13:55.947 WARN  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: This is Hello API.
2022-02-03 16:13:55.947 ERROR [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Controllers.HelloController: This is Hello API.
# 紀錄Response
2022-02-03 16:13:55.967 INFO  [http-nio-8080-exec-3] stockmarket.jovepater.com.stockmarket.Filters.HttpTransatcionLoggingFilter: Response: 200 /api/hello

小結

Filter是一個非常重要又實用的工具,決大部分的HTTP服務,不論是Web或Restful API,都需要對Request與Response預先做檢查或處理,最常見的就是一般化(Generalization)與格式檢查(Format Checking),當然還包過驗證。

~ END ~


,

Related posts

Latest posts