在 SpringBoot3.3 中拦截修改请求 Body 的多种正确方式
在 SpringBoot3.3 中拦截修改请求 Body 的多种正确方式
在现代Web应用中,安全性和数据完整性是至关重要的,尤其是在处理用户提交的数据时。请求的Body部分通常包含了关键的数据,如用户输入的表单信息、JSON数据、XML数据等,这些数据在传输和处理过程中如果没有经过适当的验证和安全检查,可能会导致严重的安全漏洞。
例如,未经处理的用户输入可能会包含恶意的 HTML 或 JavaScript 代码,攻击者可以利用这些代码在用户浏览器中执行恶意脚本,导致跨站脚本攻击(XSS)。此外,数据的完整性和准确性也可能受到篡改,这可能会导致应用程序在处理过程中出现错误或异常。
为了应对这些挑战,开发人员通常需要拦截并修改请求 Body 的内容,对其进行验证、过滤和格式化,以确保其安全性和可靠性。在Spring Boot 框架中,拦截和修改请求 Body 的方式有多种,常见的包括使用过滤器(Filter)、拦截器(Interceptor)、自定义HttpMessageConverter,以及直接在Controller中处理。
本文将深入探讨在 Spring Boot 中拦截和修改请求 Body 的多种正确方式,结合代码示例对每种方式进行详细讲解,并特别强调如何通过格式化和内容安全性检测来防止 XSS 攻击,确保应用程序的安全性和数据的完整性。我们还将介绍如何在这些方法中集成内容安全策略,增强对 HTML 和 JavaScript 标签的检测和处理,以防止潜在的安全威胁。这些技术不仅适用于一般的 Web 应用开发,还对构建高安全性的企业级应用有着重要的指导意义。
运行效果:
若想获取项目完整代码以及其他文章的项目源码,且在代码编写时遇到问题需要咨询交流,欢迎加入下方的知识星球。
项目结构
我们将创建一个 Spring Boot 项目,其中包含以下文件和配置:
pom.xml
:项目依赖配置application.yml
:项目属性配置前端页面:使用 Thymeleaf 模板引擎,结合 Bootstrap 进行样式美化
控制器、过滤器、中间件:实现拦截和修改请求 Body 的功能
项目依赖配置(pom.xml)
首先,在pom.xml
文件中添加必要的依赖项。我们的项目需要以下依赖:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.icoderoad</groupId>
<artifactId>request-body</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>request-body</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Thymeleaf -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- Apache Commons Text (for string escape operations) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
<!-- Lombok (optional for reducing boilerplate code) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目配置(application.yml)
接下来,在application.yml
文件中进行一些基本配置。通常,我们可以在这里配置服务器端口、日志级别等信息:
server:
port: 8080
spring:
thymeleaf:
cache: false
mode: HTML
suffix: .html
prefix: classpath:/templates/
四、前端页面(Thymeleaf模板)
为了演示请求拦截和修改的效果,我们可以创建一个简单的表单页面index.html
,用户可以在此页面上提交数据。
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>请求表单</title>
<!-- 引入Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- 引入jQuery -->
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<div class="container">
<h2>提交请求表单</h2>
<form id="requestForm">
<div class="mb-3">
<label for="inputData" class="form-label">输入数据</label>
<textarea class="form-control" id="inputData" rows="3" required></textarea>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
<!-- 提示信息 -->
<div id="resultMessage" class="alert mt-3" role="alert" style="display:none;"></div>
</div>
<script>
$(document).ready(function() {
$("#requestForm").on("submit", function(event) {
event.preventDefault(); // 阻止表单的默认提交行为
// 获取用户输入的数据
var inputData = $("#inputData").val();
// 使用 jQuery 发送 AJAX 请求
$.ajax({
url: "/submit",
type: "POST",
contentType: "application/json",
data: JSON.stringify({data: inputData}),
success: function(response) {
// 成功后在页面上显示提示信息
$("#resultMessage").removeClass("alert-danger").addClass("alert-success")
.text("提交成功: " + response.message)
.show();
},
error: function(xhr, status, error) {
// 失败时显示错误信息
$("#resultMessage").removeClass("alert-success").addClass("alert-danger")
.text("提交失败: " + xhr.responseText)
.show();
}
});
});
});
</script>
</body>
</html>
拦截和修改请求Body的实现方式
创建过滤器配置类
创建一个 FilterConfig
配置类,在该类中注册 RequestBodyFilter
过滤器。
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<RequestBodyFilter> requestBodyFilterRegistration() {
FilterRegistrationBean<RequestBodyFilter> registrationBean = new FilterRegistrationBean<>();
// 将自定义过滤器注册为Bean
registrationBean.setFilter(new RequestBodyFilter());
// 过滤器应用于所有URL
registrationBean.addUrlPatterns("/*");
// 设置过滤器的优先级
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
}
使用过滤器(Filter)拦截请求Body
过滤器是一种常见的拦截HTTP请求的方式。我们可以通过实现jakarta.servlet.Filter
接口来拦截请求并修改请求体。
package com.icoderoad.request_body.filter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.apache.commons.text.StringEscapeUtils;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
@Component
public class RequestBodyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest && response instanceof HttpServletResponse) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 使用自定义 HttpServletRequestWrapper 包装请求
CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper(httpRequest);
// 确保请求体内容被读取并缓存
String originalBody = wrappedRequest.getBody();
// 确认请求体内容
System.out.println("Original Request Body: " + originalBody);
// 对内容进行安全性处理:转义HTML和JavaScript标签
String sanitizedBody = sanitizeBody(originalBody);
// 将处理后的内容作为新的输入流返回
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(sanitizedBody.getBytes(StandardCharsets.UTF_8));
HttpServletRequest sanitizedRequest = new CustomHttpServletRequestWrapper(wrappedRequest) {
@Override
public ServletInputStream getInputStream() throws IOException {
return new CustomServletInputStream(byteArrayInputStream);
}
};
// 继续过滤链
chain.doFilter(sanitizedRequest, response);
} else {
chain.doFilter(request, response);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化Filter所需资源
}
@Override
public void destroy() {
// 释放Filter所占用的资源
}
// 用于安全处理请求体内容
private String sanitizeBody(String originalBody) {
String sanitizedBody = StringEscapeUtils.escapeHtml4(originalBody);
sanitizedBody = sanitizedBody.replaceAll("(?i)<script", "<script")
.replaceAll("(?i)</script", "</script");
ObjectMapper objectMapper = new ObjectMapper();
try {
JsonNode jsonNode = objectMapper.readTree(originalBody);
// 对 JSON 数据进行处理(比如移除不必要的字段)
return objectMapper.writeValueAsString(jsonNode);
} catch (IOException e) {
e.printStackTrace();
// 处理 JSON 解析异常
return sanitizedBody;
}
}
// 自定义ServletInputStream类,简化流操作
private static class CustomServletInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public CustomServletInputStream(ByteArrayInputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(jakarta.servlet.ReadListener readListener) {
// 读取监听器设置,当前未实现
}
}
// 自定义 HttpServletRequestWrapper 类
private static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 读取请求体内容并缓存
InputStream inputStream = request.getInputStream();
body = inputStream.readAllBytes();
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CustomServletInputStream(new ByteArrayInputStream(body));
}
public String getBody() {
return new String(body, StandardCharsets.UTF_8);
}
}
}
使用Spring Interceptor拦截请求Body
Spring的拦截器(Interceptor)是另一种拦截HTTP请求的方式。它比过滤器更接近Spring的处理机制。
package com.icoderoad.request_body.interceptor;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import org.apache.commons.text.StringEscapeUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.util.ContentCachingRequestWrapper;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ReadListener;
import jakarta.servlet.ServletInputStream;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
public class RequestBodyInterceptor implements HandlerInterceptor {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
// 使用 ContentCachingRequestWrapper 包装 HttpServletRequest
ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request);
// 读取请求体内容
String originalBody = readRequestBody(cachingRequest);
if (originalBody == null || originalBody.isEmpty()) {
return true; // 如果没有请求体,直接返回
}
System.out.println("Original Request Body: " + originalBody);
// 处理请求体内容
String sanitizedBody = sanitizeBody(originalBody);
// 使用自定义 HttpServletRequestWrapper 包装请求
HttpServletRequest wrappedRequest = new CustomHttpServletRequestWrapper(cachingRequest, sanitizedBody);
// 替换请求对象
request.setAttribute("wrappedRequest", wrappedRequest);
return true;
}
private String readRequestBody(HttpServletRequest request) throws IOException {
ContentCachingRequestWrapper wrapper = (ContentCachingRequestWrapper) request;
byte[] buf = wrapper.getContentAsByteArray();
if (buf.length > 0) {
return new String(buf, 0, buf.length, wrapper.getCharacterEncoding());
}
return null;
}
private String sanitizeBody(String originalBody) {
// 如果请求体是 JSON 格式,则对其进行特殊处理
if (isJson(originalBody)) {
try {
JsonNode jsonNode = objectMapper.readTree(originalBody);
// 对 JSON 数据进行处理(比如移除不必要的字段)
return objectMapper.writeValueAsString(jsonNode);
} catch (IOException e) {
// 捕获 JSON 解析异常,记录错误并返回原始内容
e.printStackTrace();
return originalBody; // 返回原始内容
}
} else {
// 对内容进行安全性处理:转义HTML和JavaScript标签
String sanitizedBody = StringEscapeUtils.escapeHtml4(originalBody);
sanitizedBody = sanitizedBody.replaceAll("(?i)<script", "<script")
.replaceAll("(?i)</script", "</script>");
return sanitizedBody;
}
}
private boolean isJson(String content) {
// 简单检查内容是否是 JSON 格式
return content.trim().startsWith("{") || content.trim().startsWith("[");
}
private static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final ByteArrayInputStream inputStream;
public CustomHttpServletRequestWrapper(HttpServletRequest request, String body) {
super(request);
this.inputStream = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
}
@Override
public ServletInputStream getInputStream() throws IOException {
return new CustomServletInputStream(inputStream);
}
@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
}
}
private static class CustomServletInputStream extends ServletInputStream {
private final ByteArrayInputStream inputStream;
public CustomServletInputStream(ByteArrayInputStream inputStream) {
this.inputStream = inputStream;
}
@Override
public int read() throws IOException {
return inputStream.read();
}
@Override
public boolean isFinished() {
return inputStream.available() == 0;
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setReadListener(ReadListener readListener) {
// 读取监听器设置,当前未实现
}
}
}
创建配置类来注册拦截器
在你的 Spring Boot 项目中创建一个配置类,配置 RequestBodyInterceptor
:
package com.icoderoad.request_body.config;
import java.util.List;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.icoderoad.request_body.converter.CustomHttpMessageConverter;
import com.icoderoad.request_body.interceptor.RequestBodyInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new RequestBodyInterceptor());
}
@Bean
public CustomHttpMessageConverter customHttpMessageConverter(ObjectMapper objectMapper) {
return new CustomHttpMessageConverter(objectMapper);
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// 移除默认的 Jackson 2 HttpMessageConverter
converters.removeIf(converter -> converter instanceof AbstractHttpMessageConverter);
// 添加自定义的 HttpMessageConverter
converters.add(customHttpMessageConverter(new ObjectMapper()));
}
}
使用自定义HttpMessageConverter
Spring提供了HttpMessageConverter
来处理请求体的转换。我们可以自定义一个HttpMessageConverter
来拦截和修改请求体。
package com.icoderoad.request_body.converter;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.commons.text.StringEscapeUtils;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType;
import org.springframework.http.converter.AbstractHttpMessageConverter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class CustomHttpMessageConverter extends AbstractHttpMessageConverter<Object> {
private final ObjectMapper objectMapper;
public CustomHttpMessageConverter(ObjectMapper objectMapper) {
super(MediaType.APPLICATION_JSON);
this.objectMapper = objectMapper;
}
@Override
protected boolean supports(Class<?> clazz) {
return true; // 支持所有类
}
@Override
protected Object readInternal(Class<? extends Object> clazz, HttpInputMessage inputMessage) throws IOException {
String body = new String(inputMessage.getBody().readAllBytes(), StandardCharsets.UTF_8);
// 处理请求体内容
String sanitizedBody = sanitizeBody(body);
// 将处理后的内容转换为对象
return objectMapper.readValue(sanitizedBody, clazz);
}
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException {
// 将对象转换为 JSON 字符串
String json = objectMapper.writeValueAsString(object);
// 输出处理后的 JSON 字符串
outputMessage.getBody().write(json.getBytes(StandardCharsets.UTF_8));
}
private String sanitizeBody(String originalBody) {
// 如果请求体是 JSON 格式,则对其进行特殊处理
if (originalBody.trim().startsWith("{") || originalBody.trim().startsWith("[")) {
try {
JsonNode jsonNode = objectMapper.readTree(originalBody);
// 对 JSON 数据进行处理(比如移除不必要的字段)
return objectMapper.writeValueAsString(jsonNode);
} catch (IOException e) {
// 捕获 JSON 解析异常,记录错误并返回原始内容
e.printStackTrace();
return originalBody; // 返回原始内容
}
} else {
// 对内容进行安全性处理:转义HTML和JavaScript标签
String sanitizedBody = StringEscapeUtils.escapeHtml4(originalBody);
sanitizedBody = sanitizedBody.replaceAll("(?i)<script", "<script")
.replaceAll("(?i)</script", "</script>");
return sanitizedBody;
}
}
}
在Controller中直接修改请求Body
最后一种方式是在Controller中直接读取并修改请求体。
package com.icoderoad.request_body.controller;
import java.util.HashMap;
import java.util.Map;
import org.apache.commons.text.StringEscapeUtils;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class RequestController {
@PostMapping("/submit")
public ResponseEntity<Map<String, String>> submit(@RequestBody Map<String, String> requestData) {
String data = requestData.get("data");
String sanitizedBody = StringEscapeUtils.escapeHtml4(data);
sanitizedBody = sanitizedBody.replaceAll("(?i)<script", "<script")
.replaceAll("(?i)</script", "</script");
// 在此处理接收到的数据(例如存储、验证等)
// 这里我们假设处理成功并返回一条消息
Map<String, String> response = new HashMap<>();
response.put("message", "接收到的数据: " + sanitizedBody);
// 返回200 OK响应和响应消息
return ResponseEntity.ok(response);
}
}
总结
以上介绍了在 Spring Boot3.3 中拦截和修改请求 Body 的多种方式,包括使用过滤器、拦截器、HttpMessageConverter 以及在控制器中直接修改请求体。每种方式都有其适用场景,可以根据实际需求选择合适的方式。希望本文对大家在实际开发中有所帮助。