知識庫 / Spring / Spring Boot RSS 訂閱

在 Spring Boot 中修改請求體在到達控制器之前

Spring Boot
HongKong
5
11:21 AM · Dec 06 ,2025

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", "&lt;script&gt;alert()&lt;/script&gt;[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", "&lt;script&gt;alert()&lt;/script&gt;[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 請求體到達控制器之前對其進行了修改。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.