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 ~