查看原文
其他

在 SpringBoot3.3 中拦截修改请求 Body 的多种正确方式

编程疏影 路条编程
2024-09-04


在 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", "&lt;script")
.replaceAll("(?i)</script", "&lt;/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", "&lt;script")
.replaceAll("(?i)</script", "&lt;/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", "&lt;script")
.replaceAll("(?i)</script", "&lt;/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", "&lt;script")
.replaceAll("(?i)</script", "&lt;/script");


// 在此处理接收到的数据(例如存储、验证等)
// 这里我们假设处理成功并返回一条消息

Map<String, String> response = new HashMap<>();
response.put("message", "接收到的数据: " + sanitizedBody);

// 返回200 OK响应和响应消息
return ResponseEntity.ok(response);
}
}

总结

以上介绍了在 Spring Boot3.3 中拦截和修改请求 Body 的多种方式,包括使用过滤器、拦截器、HttpMessageConverter 以及在控制器中直接修改请求体。每种方式都有其适用场景,可以根据实际需求选择合适的方式。希望本文对大家在实际开发中有所帮助。


今天就讲到这里,如果有问题需要咨询,大家可以直接留言或扫下方二维码来知识星球找我,我们会尽力为你解答。


AI资源聚合站已经正式上线,该平台不仅仅是一个AI资源聚合站,更是一个为追求知识深度和广度的人们打造的智慧聚集地。通过访问 AI 资源聚合网站 https://ai-ziyuan.techwisdom.cn/,你将进入一个全方位涵盖人工智能和语言模型领域的宝藏库。


作者:路条编程(转载请获本公众号授权,并注明作者与出处)


继续滑动看下一个
路条编程
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存