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

开发 前端
经处理的用户输入可能会包含恶意的 HTML 或 JavaScript 代码,攻击者可以利用这些代码在用户浏览器中执行恶意脚本,导致跨站脚本攻击(XSS)。

在现代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 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模板)

为了演示请求拦截和修改的效果,我们可以创建一个简单的表单页面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 以及在控制器中直接修改请求体。每种方式都有其适用场景,可以根据实际需求选择合适的方式。希望本文对大家在实际开发中有所帮助。

责任编辑:武晓燕 来源: 路条编程
相关推荐

2024-09-04 11:16:44

端口Spring配置类

2024-08-30 11:28:09

2024-09-06 10:05:47

SpELSpring权限

2024-09-03 10:44:32

2020-03-25 17:55:30

SpringBoot拦截器Java

2024-08-02 08:21:52

Spring项目方式

2012-08-13 10:23:33

IBMdW

2011-02-23 10:35:04

Konqueror

2024-09-09 11:35:35

2023-03-10 22:14:49

KustomizeKubernetes

2024-10-15 10:38:32

2018-09-17 08:31:08

容器Docker雪球

2018-06-19 08:12:25

2024-09-05 09:35:58

CGLIBSpring动态代理

2024-04-09 08:04:42

C#结构await

2009-11-23 17:16:54

PHP获取IP

2024-09-26 09:28:06

内存Spring

2024-01-23 08:47:13

BeanSpring加载方式

2023-09-14 08:16:51

2022-07-03 20:53:23

React18请求数据
点赞
收藏

51CTO技术栈公众号