概述
在这个教程中,我们将学习如何在Spring Boot应用中修改HTTP请求,使其在到达控制器之前进行处理。Web应用和RESTful web服务经常采用这种方法来处理常见的问题,如对进入的实际控制器之前的HTTP请求进行转换或增强。这有助于实现松耦合,并显著减少开发工作量。
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);
}
}
@Order(1)
注解中的值1
表示所有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()
方法对请求体进行修改后,分配了新的内容。
接下来,定义一个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;
}
}
对于这个演示,控制器在*/save*端点上接收并返回接收到的请求体user
。
让我们看看过滤器是否起作用:
@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)));
}
很好,过滤器成功地在请求到达UserController
类定义的/save
URL之前转义了HTML字符。
3. 使用Spring AOP
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;
}
}
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的ApplicationContext
**。
让我们看看拦截器是如何工作的:
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()
方法。在方法中,我们使用addPathPatterns()
方法为所有入站HTTP请求注册了拦截器EscapeHtmlRequestInterceptor
。
令人惊讶的是,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请求体。
如往常一样,示例代码可以在GitHub上找到。