在 SpringBoot3.3 中攔截修改請求 Body 的多種正確方式
在現(xiàn)代Web應用中,安全性和數(shù)據(jù)完整性是至關(guān)重要的,尤其是在處理用戶提交的數(shù)據(jù)時。請求的Body部分通常包含了關(guān)鍵的數(shù)據(jù),如用戶輸入的表單信息、JSON數(shù)據(jù)、XML數(shù)據(jù)等,這些數(shù)據(jù)在傳輸和處理過程中如果沒有經(jīng)過適當?shù)尿炞C和安全檢查,可能會導致嚴重的安全漏洞。
例如,未經(jīng)處理的用戶輸入可能會包含惡意的 HTML 或 JavaScript 代碼,攻擊者可以利用這些代碼在用戶瀏覽器中執(zhí)行惡意腳本,導致跨站腳本攻擊(XSS)。此外,數(shù)據(jù)的完整性和準確性也可能受到篡改,這可能會導致應用程序在處理過程中出現(xiàn)錯誤或異常。
為了應對這些挑戰(zhàn),開發(fā)人員通常需要攔截并修改請求 Body 的內(nèi)容,對其進行驗證、過濾和格式化,以確保其安全性和可靠性。在Spring Boot 框架中,攔截和修改請求 Body 的方式有多種,常見的包括使用過濾器(Filter)、攔截器(Interceptor)、自定義HttpMessageConverter,以及直接在Controller中處理。
本文將深入探討在 Spring Boot 中攔截和修改請求 Body 的多種正確方式,結(jié)合代碼示例對每種方式進行詳細講解,并特別強調(diào)如何通過格式化和內(nèi)容安全性檢測來防止 XSS 攻擊,確保應用程序的安全性和數(shù)據(jù)的完整性。我們還將介紹如何在這些方法中集成內(nèi)容安全策略,增強對 HTML 和 JavaScript 標簽的檢測和處理,以防止?jié)撛诘陌踩{。這些技術(shù)不僅適用于一般的 Web 應用開發(fā),還對構(gòu)建高安全性的企業(yè)級應用有著重要的指導意義。
運行效果:
若想獲取項目完整代碼以及其他文章的項目源碼,且在代碼編寫時遇到問題需要咨詢交流,歡迎加入下方的知識星球。
項目結(jié)構(gòu)
我們將創(chuàng)建一個 Spring Boot 項目,其中包含以下文件和配置:
- pom.xml:項目依賴配置
- application.yml:項目屬性配置
- 前端頁面:使用 Thymeleaf 模板引擎,結(jié)合 Bootstrap 進行樣式美化
- 控制器、過濾器、中間件:實現(xiàn)攔截和修改請求 Body 的功能
項目依賴配置(pom.xml)
首先,在pom.xml文件中添加必要的依賴項。我們的項目需要以下依賴:
<?xml versinotallow="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模板)
為了演示請求攔截和修改的效果,我們可以創(chuàng)建一個簡單的表單頁面index.html,用戶可以在此頁面上提交數(shù)據(jù)。
<!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 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">輸入數(shù)據(jù)</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(); // 阻止表單的默認提交行為
// 獲取用戶輸入的數(shù)據(jù)
var inputData = $("#inputData").val();
// 使用 jQuery 發(fā)送 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的實現(xiàn)方式
創(chuàng)建過濾器配置類
創(chuàng)建一個 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("/*");
// 設置過濾器的優(yōu)先級
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}
}
使用過濾器(Filter)攔截請求Body
過濾器是一種常見的攔截HTTP請求的方式。我們可以通過實現(xiàn)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);
// 確保請求體內(nèi)容被讀取并緩存
String originalBody = wrappedRequest.getBody();
// 確認請求體內(nèi)容
System.out.println("Original Request Body: " + originalBody);
// 對內(nèi)容進行安全性處理:轉(zhuǎn)義HTML和JavaScript標簽
String sanitizedBody = sanitizeBody(originalBody);
// 將處理后的內(nèi)容作為新的輸入流返回
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(sanitizedBody.getBytes(StandardCharsets.UTF_8));
HttpServletRequest sanitizedRequest = new CustomHttpServletRequestWrapper(wrappedRequest) {
@Override
public ServletInputStream getInputStream() throws IOException {
return new CustomServletInputStream(byteArrayInputStream);
}
};
// 繼續(xù)過濾鏈
chain.doFilter(sanitizedRequest, response);
} else {
chain.doFilter(request, response);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化Filter所需資源
}
@Override
public void destroy() {
// 釋放Filter所占用的資源
}
// 用于安全處理請求體內(nèi)容
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 數(shù)據(jù)進行處理(比如移除不必要的字段)
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) {
// 讀取監(jiān)聽器設置,當前未實現(xiàn)
}
}
// 自定義 HttpServletRequestWrapper 類
private static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final byte[] body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 讀取請求體內(nèi)容并緩存
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);
// 讀取請求體內(nèi)容
String originalBody = readRequestBody(cachingRequest);
if (originalBody == null || originalBody.isEmpty()) {
return true; // 如果沒有請求體,直接返回
}
System.out.println("Original Request Body: " + originalBody);
// 處理請求體內(nèi)容
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 數(shù)據(jù)進行處理(比如移除不必要的字段)
return objectMapper.writeValueAsString(jsonNode);
} catch (IOException e) {
// 捕獲 JSON 解析異常,記錄錯誤并返回原始內(nèi)容
e.printStackTrace();
return originalBody; // 返回原始內(nèi)容
}
} else {
// 對內(nèi)容進行安全性處理:轉(zhuǎn)義HTML和JavaScript標簽
String sanitizedBody = StringEscapeUtils.escapeHtml4(originalBody);
sanitizedBody = sanitizedBody.replaceAll("(?i)<script", "<script")
.replaceAll("(?i)</script", "</script>");
return sanitizedBody;
}
}
private boolean isJson(String content) {
// 簡單檢查內(nèi)容是否是 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) {
// 讀取監(jiān)聽器設置,當前未實現(xiàn)
}
}
}
創(chuàng)建配置類來注冊攔截器
在你的 Spring Boot 項目中創(chuàng)建一個配置類,配置 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來處理請求體的轉(zhuǎn)換。我們可以自定義一個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);
// 處理請求體內(nèi)容
String sanitizedBody = sanitizeBody(body);
// 將處理后的內(nèi)容轉(zhuǎn)換為對象
return objectMapper.readValue(sanitizedBody, clazz);
}
@Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException {
// 將對象轉(zhuǎn)換為 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 數(shù)據(jù)進行處理(比如移除不必要的字段)
return objectMapper.writeValueAsString(jsonNode);
} catch (IOException e) {
// 捕獲 JSON 解析異常,記錄錯誤并返回原始內(nèi)容
e.printStackTrace();
return originalBody; // 返回原始內(nèi)容
}
} else {
// 對內(nèi)容進行安全性處理:轉(zhuǎn)義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");
// 在此處理接收到的數(shù)據(jù)(例如存儲、驗證等)
// 這里我們假設處理成功并返回一條消息
Map<String, String> response = new HashMap<>();
response.put("message", "接收到的數(shù)據(jù): " + sanitizedBody);
// 返回200 OK響應和響應消息
return ResponseEntity.ok(response);
}
}
總結(jié)
以上介紹了在 Spring Boot3.3 中攔截和修改請求 Body 的多種方式,包括使用過濾器、攔截器、HttpMessageConverter 以及在控制器中直接修改請求體。每種方式都有其適用場景,可以根據(jù)實際需求選擇合適的方式。希望本文對大家在實際開發(fā)中有所幫助。