Modify Request Body Before Reaching Controller in Spring Boot – 在 Spring Boot 中,在到达控制器之前修改请求体

最后修改: 2023年 11月 30日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this tutorial, we’ll learn how to modify an HTTP request before it reaches the controller in a Spring Boot application. Web applications and RESTful web services often employ this technique to address common concerns like transforming or enriching the incoming HTTP requests before they hit the actual controllers. This promotes loose coupling and considerably reduces development effort.

在本教程中,我们将学习如何在 HTTP 请求到达 Spring Boot 应用程序的控制器之前对其进行修改。Web 应用程序和 RESTful Web 服务经常使用这种技术来解决常见问题,例如在传入的 HTTP 请求到达实际控制器之前对其进行转换或丰富。这促进了松散耦合,大大减少了开发工作量。

2. Modify Request With Filters

2.使用过滤器修改请求

Often, applications have to perform generic operations such as authentication, logging, escaping HTML characters, etc. Filters are an excellent choice to take care of these generic concerns of an application running in any servlet container. Let’s take a look at how a filter works:

通常,应用程序必须执行一些通用操作,如身份验证、日志记录、转义 HTML 字符等。Filters 是处理在任何 servlet 容器中运行的应用程序的这些通用问题的最佳选择。让我们来看看过滤器是如何工作的:

 

filter sequence design

In Spring Boot applications, filters can be registered to be invoked in a particular order to:

在 Spring Boot 应用程序中,可以注册过滤器,以便按特定顺序调用,从而: <br

  • modify the request
  • log the request
  • check the request for authentication or some malicious scripts
  • decide to reject or forward the request to the next filter or controller

Let’s assume we want to escape all the HTML characters from the HTTP request body to prevent an XSS attack. Let’s first define the filter:

假设我们要转义 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);
    }
}

The value 1  in the @Order annotation signifies that all HTTP requests first pass through the filter EscapeHtmlFilter. We can also register filters with the help of FilterRegistrationBean defined in the Spring Boot configuration class. With this, we can define the URL pattern for the filter as well.

@Order 注解中的值1表示所有 HTTP 请求首先通过过滤器EscapeHtmlFilter。我们还可以借助 Spring Boot 配置类中定义的 FilterRegistrationBean 注册过滤器。有了它,我们还可以为过滤器定义 URL 模式。

The doFilter() method wraps the original ServletRequest in a custom wrapper EscapeHtmlRequestWrapper:

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()));
    }
}

The wrapper is necessary because we cannot modify the original HTTP request. Without this, the servlet container would reject the request.

由于我们无法修改原始 HTTP 请求,所以封装器是必要的。如果不这样做,Servlet 容器就会拒绝请求

In the custom wrapper, we’ve overridden the method getInputStream() to return a new ServletInputStream. Basically, we assigned it the modified request body after escaping the HTML characters with the method escapeHtml().

在自定义封装器中,我们重载了方法 getInputStream() 以返回一个新的 ServletInputStream 。基本上,在使用方法 escapeHtml() 转义 HTML 字符后,我们将修改后的请求正文分配给了它。

Let’s define a UserController class:

让我们定义一个 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;
    }
}

For this demo, the controller returns the request body user that it receives on the endpoint /save.

在此演示中,控制器会返回它在端点 /save 上接收到的请求体 user

Let’s see if the filter works:

让我们看看过滤器是否有效:

@Test
void givenFilter_whenEscapeHtmlFilter_thenEscapeHtml() throws Exception {

    Map<String, String> requestBody = Map.of(
      "name", "James Cameron",
      "email", "<script>alert()</script>james@gmail.com"
    );

    Map<String, String> expectedResponseBody = Map.of(
      "name", "James Cameron",
      "email", "&lt;script&gt;alert()&lt;/script&gt;james@gmail.com"
    );

    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)));
}

Well, the filter successfully escapes the HTML characters before it hits the URL /save defined in the UserController class.

好了,过滤器在进入 UserController 类中定义的 URL /save 之前,成功地转义了 HTML 字符。

3. Using Spring AOP

3.使用 Spring AOP

The RequestBodyAdvice interface along with the annotation @RestControllerAdvice by the Spring framework helps apply global advice to all REST controllers in a Spring application. Let’s use them to escape the HTML characters from the HTTP request before it reaches the controllers:

Spring 框架的 RequestBodyAdvice 接口和注解 @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;
    }
}

The method beforeBodyRead() gets called before the HTTP request hits the controller. Hence we’re escaping the HTML characters in it. The support() method returns true which means it applies the advice to all the REST controllers.

方法 beforeBodyRead() 会在 HTTP 请求到达控制器之前被调用。因此,我们在其中转义了 HTML 字符。support()方法返回true,这意味着它会将建议应用于所有 REST 控制器

Let’s see if it works:

让我们看看它是否有效:

@Test
void givenAspect_whenEscapeHtmlAspect_thenEscapeHtml() throws Exception {

    Map<String, String> requestBody = Map.of(
      "name", "James Cameron",
      "email", "<script>alert()</script>james@gmail.com"
    );

    Map<String, String> expectedResponseBody = Map.of(
      "name", "James Cameron",
      "email", "&lt;script&gt;alert()&lt;/script&gt;james@gmail.com"
    );

    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)));
}

As expected, all the HTML characters were escaped.

不出所料,所有 HTML 字符都被转义。

We can also create custom AOP annotations which can used on controller methods to apply the advice in a more granular way.

我们还可以创建 自定义 AOP 注释,用于控制器方法,以更精细的方式应用建议。

4. Modify Request With Interceptors

4.使用拦截器修改请求

A Spring interceptor is a class that can intercept incoming HTTP requests and process them before the controller handles them. Interceptors are used for various purposes, such as authentication, authorization, logging, and caching. Moreover, interceptors are specific to the Spring MVC framework where they have access to the Spring ApplicationContext.

Spring拦截器是一个可以拦截传入 HTTP 请求并在控制器处理这些请求之前对其进行处理的类。拦截器有多种用途,例如身份验证、授权、日志记录和缓存。此外,拦截器是 Spring MVC 框架的特有功能,它们可以访问 Spring ApplicationContext

Let’s see how interceptors work:

让我们看看拦截器是如何工作的:

 

interceptor sequence design

The DispatcherServlet forwards the HTTP request to the interceptor. Further, after processing, the interceptor can forward the request to the controller or reject it. Due to this, there’s a widespread misconception that interceptors can alter HTTP requests. However, we’ll demonstrate that this notion is incorrect.

DispatcherServlet 会将 HTTP 请求转发给拦截器。此外,拦截器在处理后可将请求转发给控制器或拒绝该请求。因此,人们普遍误认为拦截器可以更改 HTTP 请求。但是,我们将证明这种观点是错误的。

Let’s consider the example of escaping the HTML characters from the HTTP requests, discussed in the earlier section. Let’s see if we can implement this with a Spring MVC interceptor:

让我们来看看前面讨论过的从 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);
    }
}

All interceptors must implement the HandleInterceptor interface. In the interceptor, the preHandle() method gets called before the request is forwarded to the target controllers. Hence, we’ve wrapped the HttpServletRequest object in the EscapeHtmlRequestWrapper and that takes care of escaping the HTML characters.

所有拦截器都必须实现 HandleInterceptor 接口。在拦截器中,preHandle() 方法将在请求转发给目标控制器之前被调用。因此,我们将 HttpServletRequest 对象封装在 EscapeHtmlRequestWrapper 中,这样就可以处理 HTML 字符的转义。

Furthermore, we must also register the interceptor to an appropriate URL pattern:

此外,我们还必须将拦截器注册为适当的 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);
    }
}

As we can see, WebMvcConfiguration class implements WebMvcConfigurer. In the class, we’ve overridden the method addInterceptors(). In the method, we registered the interceptor EscapeHtmlRequestInterceptor for all the incoming HTTP requests with the method addPathPatterns().

我们可以看到,WebMvcConfiguration类实现了WebMvcConfigurer。在该类中,我们重载了方法 addInterceptors()。在该方法中,我们使用方法 addPathPatterns() 为所有传入 HTTP 请求注册了拦截器 EscapeHtmlRequestInterceptor

Surprisingly, HtmlEscapeRequestInterceptor fails to forward the modified request body and call the handler /save:

令人惊讶的是,HtmlEscapeRequestInterceptor 未能转发修改后的请求正文并调用处理程序 /save

@Test
void givenInterceptor_whenEscapeHtmlInterceptor_thenEscapeHtml() throws Exception {
    Map<String, String> requestBody = Map.of(
      "name", "James Cameron",
      "email", "<script>alert()</script>james@gmail.com"
    );

    ObjectMapper objectMapper = new ObjectMapper();
    mockMvc.perform(MockMvcRequestBuilders.post(URI.create("/save"))
      .contentType(MediaType.APPLICATION_JSON)
      .content(objectMapper.writeValueAsString(requestBody)))
      .andExpect(MockMvcResultMatchers.status().is4xxClientError());
}

We pushed a few JavaScript characters in the HTTP request body. Unexpectedly the request fails with an HTTP error code 400. Hence, though interceptors can act like filters, they aren’t suitable for modifying the HTTP request. Rather, they’re useful when we need to modify an object in the Spring application context.

我们在 HTTP 请求正文中推送了几个 JavaScript 字符。不料,请求以 HTTP 错误代码 400 失败。因此,虽然拦截器可以像过滤器一样发挥作用,但它们并不适合修改 HTTP 请求。相反,当我们需要修改 Spring 应用上下文中的对象时,拦截器就会派上用场。

5. Conclusion

5.结论

In this article, we discussed the various ways to modify the HTTP request body in a Spring Boot application before it reaches the controller. According to popular belief, interceptors can help in doing it, but we saw that it fails. However, we saw how filters and AOP successfully modify an HTTP request body before it reaches the controller.

在本文中,我们讨论了在 Spring Boot 应用程序中,在 HTTP 请求到达控制器之前修改 HTTP 请求体的各种方法。根据流行的说法,拦截器可以帮助实现这一目标,但我们看到拦截器失败了。不过,我们看到了过滤器和 AOP 如何在 HTTP 请求到达控制器之前成功修改 HTTP 请求体。

As usual, the source code for the examples is available over on GitHub.

与往常一样,这些示例的源代码可在 GitHub 上获取。