1. 概述
在本教程中,我們將學習如何在 Spring Boot 應用程序中,在 HTTP 請求到達控制器之前對其進行修改。 Web 應用程序和 RESTful Web 服務經常採用這種技術來解決常見問題,例如在實際控制器接收請求之前對其進行轉換或增強。 這種方法有助於降低耦合度,並顯著減少開發工作量。
2. 使用過濾器修改請求
在許多應用程序中,經常需要執行諸如身份驗證、日誌記錄、轉義 HTML 字符等通用操作。過濾器是處理任何 Servlet 容器中應用程序通用關注點的絕佳選擇。 讓我們來看一下過濾器的工作原理:
在 Spring Boot 應用程序中,過濾器可以註冊以特定順序調用,以便於:**
- 修改請求
- 記錄請求
- 檢查請求以進行身份驗證或惡意腳本
- 決定拒絕或將請求轉發到下一個過濾器或控制器
假設我們想要轉義 HTTP 請求體中的所有 HTML 字符,以防止 XSS 攻擊。 讓我們首先定義過濾器:
@Component
@Order(1)
public class EscapeHtmlFilter implements Filter {
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
filterChain.doFilter(new HtmlEscapeRequestWrapper((HttpServletRequest) servletRequest), servletResponse);
}
}值 1 在 @Order 註解中的含義是所有 HTTP 請求首先通過 EscapeHtmlFilter 過濾器。我們還可以通過 Spring Boot 配置類中定義的 FilterRegistrationBean 註冊過濾器。 這樣,我們還可以為過濾器定義 URL 模式。
doFilter() 方法將原始 ServletRequest 包裝在一個自定義包裝器 EscapeHtmlRequestWrapper 中:
public class EscapeHtmlRequestWrapper extends HttpServletRequestWrapper {
private String body = null;
public HtmlEscapeRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
this.body = this.escapeHtml(request);
}
@Override
public ServletInputStream getInputStream() throws IOException {
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
ServletInputStream servletInputStream = new ServletInputStream() {
@Override
public int read() throws IOException {
return byteArrayInputStream.read();
}
//Other implemented methods...
};
return servletInputStream;
}
@Override
public BufferedReader getReader() {
return new BufferedReader(new InputStreamReader(this.getInputStream()));
}
}包裝器是必要的,因為我們無法修改原始 HTTP 請求。如果沒有它,servlet 容器會拒絕該請求。
在自定義包裝器中,我們覆蓋了 getInputStream() 方法,以返回一個新的 ServletInputStream。基本上,我們使用 escapeHtml() 方法將 HTML 字符轉義後,將其分配給修改後的請求主體。
讓我們定義一個 UserController 類:
@RestController
@RequestMapping("/")
public class UserController {
@PostMapping(value = "save")
public ResponseEntity<String> saveUser(@RequestBody String user) {
logger.info("save user info into database");
ResponseEntity<String> responseEntity = new ResponseEntity<>(user, HttpStatus.CREATED);
return responseEntity;
}
}對於本次演示,控制器返回接收到的請求體 ,該請求體在端點 上接收。
讓我們看看過濾器是否生效:
@Test
void givenFilter_whenEscapeHtmlFilter_thenEscapeHtml() throws Exception {
Map<String, String> requestBody = Map.of(
"name", "James Cameron",
"email", "<script>alert()</script>[email protected]"
);
Map<String, String> expectedResponseBody = Map.of(
"name", "James Cameron",
"email", "<script>alert()</script>[email protected]"
);
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestBody)))
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(expectedResponseBody)));
}很好,過濾器在將 HTML 字符轉義後,成功地將其發送到 /save 路徑,該路徑定義在 UserController 類中。
3. 使用 Spring AOP
RequestBodyAdvice 接口以及 Spring 框架中的 @RestControllerAdvice 註解,可以幫助你將全局建議應用到 Spring 應用程序中的所有 REST 控制器上。 讓我們使用它們在 HTTP 請求到達控制器之前,從 HTML 特殊字符中進行轉義:
@RestControllerAdvice
public class EscapeHtmlAspect implements RequestBodyAdvice {
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
InputStream inputStream = inputMessage.getBody();
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
return new ByteArrayInputStream(escapeHtml(inputStream).getBytes(StandardCharsets.UTF_8));
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}
@Override
public boolean supports(MethodParameter methodParameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object afterBodyRead(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
@Override
public Object handleEmptyBody(Object body, HttpInputMessage inputMessage,
MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
return body;
}
}方法 beforeBodyRead() 在 HTTP 請求到達控制器之前會被調用。因此,我們對其進行 HTML 字符轉義。方法 support() 返回 true,這意味着它將應用到所有 REST 控制器。
讓我們看看它是否有效:
@Test
void givenAspect_whenEscapeHtmlAspect_thenEscapeHtml() throws Exception {
Map<String, String> requestBody = Map.of(
"name", "James Cameron",
"email", "<script>alert()</script>[email protected]"
);
Map<String, String> expectedResponseBody = Map.of(
"name", "James Cameron",
"email", "<script>alert()</script>[email protected]"
);
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestBody)))
.andExpect(MockMvcResultMatchers.status().isCreated())
.andExpect(MockMvcResultMatchers.content().json(objectMapper.writeValueAsString(expectedResponseBody)));
}正如預期的那樣,所有 HTML 字符都已進行轉義。
我們還可以創建自定義 AOP 註解,這些註解可用於在更精細的粒度上應用於控制器方法中的建議。
4. 使用攔截器修改請求
Spring 攔截器是一個可以攔截傳入的 HTTP 請求並對其進行處理,在控制器處理請求之前執行的類。攔截器用於各種目的,例如身份驗證、授權、日誌記錄和緩存。 此外,攔截器是特定於 Spring MVC 框架的,它們可以訪問 Spring 的 <em ApplicationContext</em>。
讓我們看看攔截器的工作原理:
DispatcherServlet 將 HTTP 請求轉發到攔截器。 進一步地,在處理後,攔截器可以將請求轉發到控制器或拒絕它。 因此,存在一種普遍的誤解,即攔截器可以修改 HTTP 請求。 但是,我們將證明這一觀點是錯誤的。
讓我們考慮一下從 HTTP 請求中轉義 HTML 字符的例子,在前面的一節中討論過。 讓我們看看是否可以使用 Spring MVC 攔截器來實現這一點:
public class EscapeHtmlRequestInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HtmlEscapeRequestWrapper htmlEscapeRequestWrapper = new HtmlEscapeRequestWrapper(request);
return HandlerInterceptor.super.preHandle(htmlEscapeRequestWrapper, response, handler);
}
}所有攔截器必須實現 HandleInterceptor 接口。在攔截器中,preHandle() 方法會在請求轉發到目標控制器之前被調用。因此,我們已經將 HttpServletRequest 對象封裝在 EscapeHtmlRequestWrapper 中,從而處理了 HTML 字符的轉義。
此外,我們還需要將攔截器註冊到適當的 URL 模式:
@Configuration
@EnableWebMvc
public class WebMvcConfiguration implements WebMvcConfigurer {
private static final Logger logger = LoggerFactory.getLogger(WebMvcConfiguration.class);
@Override
public void addInterceptors(InterceptorRegistry registry) {
logger.info("addInterceptors() called");
registry.addInterceptor(new HtmlEscapeRequestInterceptor()).addPathPatterns("/**");
WebMvcConfigurer.super.addInterceptors(registry);
}
}如我們所見,WebMvcConfiguration 類實現了 WebMvcConfigurer。在類中,我們重寫了 addInterceptors() 方法。在該方法中,我們為所有傳入的 HTTP 請求註冊了 EscapeHtmlRequestInterceptor 攔截器,使用 addPathPatterns() 方法。
令人驚訝的是,HtmlEscapeRequestInterceptor 無法轉發修改後的請求體並調用處理器 /save 。
@Test
void givenInterceptor_whenEscapeHtmlInterceptor_thenEscapeHtml() throws Exception {
Map<String, String> requestBody = Map.of(
"name", "James Cameron",
"email", "<script>alert()</script>[email protected]"
);
ObjectMapper objectMapper = new ObjectMapper();
mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save"))
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(requestBody)))
.andExpect(MockMvcResultMatchers.status().is4xxClientError());
}我們向 HTTP 請求體中推送了一些 JavaScript 字符。意外地,請求以 HTTP 錯誤代碼 400 失敗。因此,雖然攔截器可以像過濾器一樣工作,但它們不適合修改 HTTP 請求。相反,當我們需要修改 Spring 應用上下文中的對象時,它們則很有用。
5. 結論
在本文中,我們探討了在 Spring Boot 應用程序中,在 HTTP 請求體到達控制器之前,如何對其進行各種修改的方法。根據普遍的認知,攔截器可以幫助實現這一點,但我們發現它行不通。然而,我們觀察到過濾器和 AOP 成功地在 HTTP 請求體到達控制器之前對其進行了修改。