Skip to content

请求体处理

⚡ 解决 HttpServletRequest 输入流只能读取一次的问题,并提供 XSS 攻击防护能力。

  • 🔄 流复用RequestBodyFilter 支持请求体多次读取
  • 🛡️ XSS 防护XssFilter 自动过滤恶意脚本攻击
  • 🎯 智能判断:仅对 JSON 请求进行包装,避免文件上传异常
  • 🔧 灵活配置:支持自定义执行条件和排除规则
  • 📦 自动启用:引入依赖后默认自动配置
  • ⚙️ 可扩展:支持自定义 Safelist 和过滤策略

快速开始

自动启用

引入 krismile-boot3-autoconfigure 依赖后,请求体处理功能默认自动启用

xml
<dependency>
    <groupId>host.springboot</groupId>
    <artifactId>krismile-boot3-autoconfigure</artifactId>
</dependency>

自动启用的功能

  • RequestBodyFilter - 请求体包装过滤器(仅 JSON 请求)
  • XssFilter - XSS 攻击防护过滤器(所有请求)

基础示例

场景1:多次读取请求体

java
@RestController
@RequestMapping("/api/user")
public class UserController {
    
    @PostMapping("/create")
    public BaseVO createUser(@RequestBody UserDTO userDTO, HttpServletRequest request) 
            throws IOException {
        
        // 第一次读取请求体(在 @RequestBody 绑定时已读取)
        // 通常情况下,第二次读取会抛出异常
        
        // ✅ 有了 RequestBodyFilter,可以多次读取
        String requestBody = new BufferedReader(
            new InputStreamReader(request.getInputStream())
        ).lines().collect(Collectors.joining("\n"));
        
        log.info("原始请求体: {}", requestBody);
        
        // 继续处理业务逻辑
        return R.ok(userService.createUser(userDTO));
    }
}

场景2:XSS 攻击自动防护

java
// 前端发送恶意脚本
{
    "username": "test<script>alert('XSS')</script>",
    "email": "test@example.com"
}

// ✅ XssFilter 自动清理后,Controller 接收到的数据
{
    "username": "test",  // <script> 标签已被移除
    "email": "test@example.com"
}

核心架构

工作流程

HTTP 请求

┌─────────────────────────────────────┐
│ Filter Chain(按 Order 执行)       │
├─────────────────────────────────────┤
│ 1. RequestBodyFilter (Order=-10000) │  ← 仅 JSON 请求
│    ↓                                │
│    - 判断是否为 JSON 请求            │
│    - 是 → 包装为 RequestBodyWrapper │
│    - 否 → 跳过                      │
│                                     │
│ 2. XssFilter (Order=-9000)          │  ← 所有请求
│    ↓                                │
│    - 包装为 XssWrapper              │
│    - 过滤请求参数和请求头            │
│                                     │
│ 3. 其他过滤器...                    │
└─────────────────────────────────────┘

Controller 方法执行

核心类说明

类名职责说明
RequestBodyFilter请求体包装过滤器解决输入流只能读取一次的问题
RequestBodyWrapper请求体包装器缓存请求体字节数组,支持多次读取
XssFilterXSS 攻击防护过滤器过滤恶意脚本标签
XssWrapperXSS 包装器清理请求参数和请求头中的 XSS 脚本
HttpRequestUtils请求工具类提供 isJsonRequest() 等工具方法
XssUtilsXSS 工具类基于 Jsoup 的 XSS 清理工具

执行顺序

Filter 执行顺序(Order 数字越小越先执行):
  1. RequestBodyFilter (ORDER = -10000)

  2. XssFilter (ORDER = -9000)

  3. 其他过滤器...

  4. Controller 方法执行

💡 为什么 RequestBodyFilter 优先级最高?

  • 必须在其他过滤器读取请求体之前完成包装
  • 确保所有后续过滤器和 Controller 都能多次读取请求体
  • 避免文件上传等场景下的冲突

RequestBodyFilter - 请求体包装

核心功能

解决问题:HttpServletRequest 的 InputStream 只能读取一次

实现原理

  1. 拦截请求,判断是否为 JSON 请求(Content-Type: application/json
  2. 读取原始输入流,缓存到字节数组
  3. 包装为 RequestBodyWrapper,提供基于字节数组的新输入流
  4. 后续可以多次调用 getInputStream()getReader()

工作原理

java
// RequestBodyWrapper 核心实现
public class RequestBodyWrapper extends HttpServletRequestWrapper {
    
    private final byte[] copyBody;  // 缓存的请求体字节数组
    
    public RequestBodyWrapper(HttpServletRequest request) throws IOException {
        super(request);
        // 一次性读取原始输入流并缓存
        InputStream inputStream = request.getInputStream();
        this.copyBody = copyInputStreamByte(inputStream);
    }
    
    @Override
    public ServletInputStream getInputStream() {
        // 返回基于缓存字节数组的新输入流
        return new ServletInputStream() {
            private int lastIndexRetrieved = -1;
            
            @Override
            public int read() throws IOException {
                if (!isFinished()) {
                    int index = copyBody[lastIndexRetrieved + 1];
                    lastIndexRetrieved++;
                    return index;
                }
                return -1;
            }
            
            @Override
            public boolean isFinished() {
                return lastIndexRetrieved == (copyBody.length - 1);
            }
            
            // ... 其他方法
        };
    }
    
    @Override
    public BufferedReader getReader() {
        // 返回基于缓存字节数组的新 Reader
        return new BufferedReader(
            new InputStreamReader(new ByteArrayInputStream(copyBody))
        );
    }
}

仅处理 JSON 请求

为什么只处理 JSON 请求?

因为文件上传等表单请求会使用 request.getParts() 方法,该方法内部会读取输入流。如果提前包装了请求体,会导致文件上传失败。

判断逻辑

java
public static boolean isJsonRequest(HttpServletRequest request) {
    String contentType = request.getHeader("Content-Type");
    return contentType != null && 
           contentType.contains("application/json");
}

请求类型对比

请求类型Content-Type是否包装原因
JSON 请求application/json✅ 是可以安全包装
表单请求application/x-www-form-urlencoded❌ 否参数通过 getParameter() 获取
文件上传multipart/form-data❌ 否避免影响 getParts()
其他类型-❌ 否不需要多次读取

启用与禁用

方式1:配置文件控制(推荐)

yaml
krismile:
  web:
    request-body-wrapper:
      enabled: true  # 默认启用

方式2:自定义配置类

java
@Configuration
public class CustomFilterConfig {
    
    // 方式1:完全禁用(不注册 Bean)
    // 删除或注释掉 requestBodyFilter Bean
    
    // 方式2:自定义执行条件
    @Bean
    public RequestBodyFilter.Config requestBodyFilterConfig() {
        RequestBodyFilter.Config config = new RequestBodyFilter.Config();
        // 默认只处理 JSON 请求
        // 可以自定义执行条件
        return config;
    }
}

XssFilter - XSS 攻击防护

核心功能

防护原理:基于 Jsoup 库的 Safelist 机制,清理 HTML 标签和脚本

防护范围

  • ✅ 请求参数(request.getParameter()
  • ✅ 请求头(request.getHeader()
  • ❌ 请求体(JSON)- 需要结合 @RequestBody 的校验

工作原理

java
// XssWrapper 核心实现
public class XssWrapper extends HttpServletRequestWrapper {
    
    private final List<String> excludeParamEndWith;  // 排除的参数名后缀
    private final Safelist safeList;                  // 安全标签白名单
    private final Document.OutputSettings outputSettings;
    
    @Override
    public String getParameter(String name) {
        // 1. 检查是否排除该参数
        if (excludeParamEndWith != null) {
            for (String endWith : excludeParamEndWith) {
                if (name.endsWith(endWith)) {
                    return super.getParameter(name);  // 不过滤
                }
            }
        }
        
        // 2. 清理参数名
        name = XssUtils.clean(name, safeList, outputSettings);
        
        // 3. 清理参数值
        String value = super.getParameter(name);
        if (StringUtils.isNotBlank(value)) {
            value = XssUtils.clean(value, safeList, outputSettings);
        }
        
        return value;
    }
    
    @Override
    public String getHeader(String name) {
        // 同样清理请求头名称和值
        name = XssUtils.clean(name, safeList, outputSettings);
        String value = super.getHeader(name);
        if (StringUtils.isNotBlank(value)) {
            value = XssUtils.clean(value, safeList, outputSettings);
        }
        return value;
    }
}

XSS 清理示例

示例1:清理脚本标签

java
// 输入
String input = "Hello<script>alert('XSS')</script>World";

// 使用默认 Safelist.none()(不允许任何 HTML 标签)
String output = XssUtils.clean(input);

// 输出
"HelloWorld"  // <script> 标签被完全移除

示例2:允许特定 HTML 标签

java
// 输入
String input = "<p>Hello</p><script>alert('XSS')</script><b>World</b>";

// 允许 <p> 和 <b> 标签
Safelist safelist = Safelist.basic();
String output = XssUtils.clean(input, safelist);

// 输出
"<p>Hello</p><b>World</b>"  // 只保留安全标签

示例3:排除特定参数

java
// 配置排除 "content" 和 "html" 结尾的参数
XssFilter.Config config = new XssFilter.Config()
    .setExcludeParamEndWith(Arrays.asList("content", "html"));

// 请求:?username=<script>alert()</script>&articleContent=<p>文章内容</p>

// 结果:
// username = ""  (被清理)
// articleContent = "<p>文章内容</p>"  (不清理)

Safelist 配置

Jsoup 提供了多种预定义的 Safelist:

Safelist允许的标签适用场景
Safelist.none()不允许任何 HTML默认,最严格,推荐
Safelist.simpleText()仅允许 <b>, <em>, <i>, <strong>, <u>简单文本格式
Safelist.basic()允许基本格式标签评论、简单富文本
Safelist.basicWithImages()基本标签 + 图片支持图片的富文本
Safelist.relaxed()允许大部分标签完整富文本编辑器

自定义 Safelist

java
@Bean
public XssFilter.Config xssFilterConfig() {
    // 创建自定义白名单
    Safelist safelist = new Safelist()
        .addTags("p", "br", "b", "i", "strong", "em")  // 允许的标签
        .addAttributes("a", "href", "title")           // 允许 a 标签的属性
        .addProtocols("a", "href", "http", "https");  // 允许的协议
    
    return new XssFilter.Config()
        .setSafeList(safelist)
        .setExcludeParamEndWith(Arrays.asList("content", "html"));
}

启用与禁用

方式1:配置文件控制(推荐)

yaml
krismile:
  web:
    xss:
      enabled: true  # 默认启用

方式2:自定义配置

java
@Configuration
public class XssConfig {
    
    @Bean
    public XssFilter.Config xssFilterConfig() {
        return new XssFilter.Config()
            // 自定义执行条件(默认处理所有请求)
            .setExecutePredicate(request -> {
                // 仅对特定路径启用 XSS 过滤
                String uri = request.getRequestURI();
                return uri.startsWith("/api/public/");
            })
            // 排除特定参数
            .setExcludeParamEndWith(Arrays.asList(
                "content",      // 文章内容
                "html",         // HTML 内容
                "richText"      // 富文本
            ))
            // 自定义安全标签白名单
            .setSafeList(Safelist.basic())
            // 输出配置(禁用格式化)
            .setOutputSettings(
                new Document.OutputSettings().prettyPrint(false)
            );
    }
}

常见场景

场景1:AOP 日志记录

在 AOP 中读取请求体用于日志记录:

java
@Aspect
@Component
public class RequestLogAspect {
    
    @Around("@annotation(org.springframework.web.bind.annotation.PostMapping)")
    public Object logRequest(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = 
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();
        
        // ✅ 有了 RequestBodyFilter,可以在 AOP 中读取请求体
        if (HttpRequestUtils.isJsonRequest(request)) {
            String requestBody = new BufferedReader(
                new InputStreamReader(request.getInputStream())
            ).lines().collect(Collectors.joining("\n"));
            
            log.info("请求体: {}", requestBody);
        }
        
        // Controller 中的 @RequestBody 仍然可以正常绑定
        return point.proceed();
    }
}

场景2:接口签名验证

在过滤器中读取请求体进行签名验证:

java
@Component
public class SignatureFilter extends OncePerRequestFilter {
    
    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain) throws ServletException, IOException {
        
        if (HttpRequestUtils.isJsonRequest(request)) {
            // ✅ 读取请求体计算签名
            String requestBody = new BufferedReader(
                new InputStreamReader(request.getInputStream())
            ).lines().collect(Collectors.joining("\n"));
            
            String signature = request.getHeader("X-Signature");
            
            // 验证签名
            if (!verifySignature(requestBody, signature)) {
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                return;
            }
        }
        
        // ✅ Controller 中仍然可以正常使用 @RequestBody
        filterChain.doFilter(request, response);
    }
    
    private boolean verifySignature(String body, String signature) {
        // 签名验证逻辑
        return true;
    }
}

场景3:富文本编辑器内容保存

某些字段需要保留 HTML 标签:

java
// 配置排除规则
@Bean
public XssFilter.Config xssFilterConfig() {
    return new XssFilter.Config()
        // 排除富文本字段
        .setExcludeParamEndWith(Arrays.asList(
            "Content",      // 文章内容
            "Html",         // HTML 内容
            "RichText"      // 富文本
        ))
        // 或使用自定义 Safelist 允许特定标签
        .setSafeList(Safelist.relaxed());
}

// Controller
@PostMapping("/article/create")
public BaseVO createArticle(@RequestBody ArticleDTO dto) {
    // dto.getArticleContent() 保留了 HTML 标签
    // dto.getTitle() 被 XSS 过滤
    return R.ok(articleService.create(dto));
}

场景4:API 网关统一日志

在网关层记录所有请求体:

java
@Component
public class GatewayRequestLogFilter implements Filter {
    
    @Override
    public void doFilter(
            ServletRequest request,
            ServletResponse response,
            FilterChain chain) throws IOException, ServletException {
        
        if (request instanceof HttpServletRequest httpRequest) {
            if (HttpRequestUtils.isJsonRequest(httpRequest)) {
                // ✅ 读取请求体用于网关日志
                String requestBody = new BufferedReader(
                    new InputStreamReader(httpRequest.getInputStream())
                ).lines().collect(Collectors.joining("\n"));
                
                // 记录到 ELK
                logToElk(httpRequest.getRequestURI(), requestBody);
            }
        }
        
        // ✅ 下游服务仍可正常读取请求体
        chain.doFilter(request, response);
    }
    
    private void logToElk(String uri, String body) {
        // 发送到 ELK
    }
}

场景5:敏感数据脱敏

在日志中脱敏敏感字段:

java
@Aspect
@Component
public class DataMaskingAspect {
    
    @Around("execution(* com.example.controller..*(..))")
    public Object maskSensitiveData(ProceedingJoinPoint point) throws Throwable {
        HttpServletRequest request = 
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes())
                .getRequest();
        
        if (HttpRequestUtils.isJsonRequest(request)) {
            // ✅ 读取请求体
            String requestBody = new BufferedReader(
                new InputStreamReader(request.getInputStream())
            ).lines().collect(Collectors.joining("\n"));
            
            // 脱敏处理
            String maskedBody = maskSensitiveFields(requestBody);
            log.info("请求体(已脱敏): {}", maskedBody);
        }
        
        return point.proceed();
    }
    
    private String maskSensitiveFields(String body) {
        // 脱敏手机号、身份证等
        return body.replaceAll("\"phone\":\"(\\d{3})\\d{4}(\\d{4})\"", 
                               "\"phone\":\"$1****$2\"");
    }
}

最佳实践

✅ 推荐做法

java
// 1. 使用工具方法判断是否为 JSON 请求
if (HttpRequestUtils.isJsonRequest(request)) {
    // 安全读取请求体
    String body = readRequestBody(request);
}

// 2. 读取请求体后使用 try-with-resources
try (BufferedReader reader = request.getReader()) {
    String body = reader.lines().collect(Collectors.joining("\n"));
    // 处理请求体
}

// 3. 富文本字段配置排除规则
@Bean
public XssFilter.Config xssFilterConfig() {
    return new XssFilter.Config()
        .setExcludeParamEndWith(Arrays.asList(
            "Content", "Html", "RichText"
        ));
}

// 4. 生产环境使用严格的 Safelist
@Bean
@Profile("prod")
public XssFilter.Config prodXssConfig() {
    return new XssFilter.Config()
        .setSafeList(Safelist.none());  // 最严格
}

// 5. 自定义执行条件,减少不必要的包装
@Bean
public XssFilter.Config xssFilterConfig() {
    return new XssFilter.Config()
        .setExecutePredicate(request -> {
            // 仅对 /api/ 开头的接口启用
            return request.getRequestURI().startsWith("/api/");
        });
}

❌ 不推荐做法

java
// ❌ 1. 对非 JSON 请求读取输入流
if (!HttpRequestUtils.isJsonRequest(request)) {
    // 错误:表单请求、文件上传不应读取输入流
    String body = readRequestBody(request);
}

// ✅ 正确做法
if (HttpRequestUtils.isJsonRequest(request)) {
    String body = readRequestBody(request);
}

// ❌ 2. 忘记关闭流
BufferedReader reader = request.getReader();
String body = reader.lines().collect(Collectors.joining("\n"));
// reader 未关闭

// ✅ 正确做法
try (BufferedReader reader = request.getReader()) {
    String body = reader.lines().collect(Collectors.joining("\n"));
}

// ❌ 3. 对所有字段都使用 Safelist.relaxed()
@Bean
public XssFilter.Config xssFilterConfig() {
    return new XssFilter.Config()
        .setSafeList(Safelist.relaxed());  // 太宽松,不安全
}

// ✅ 正确做法:仅对需要的字段使用宽松策略
@Bean
public XssFilter.Config xssFilterConfig() {
    return new XssFilter.Config()
        .setSafeList(Safelist.none())  // 默认严格
        .setExcludeParamEndWith(Arrays.asList("Content"));  // 特定字段排除
}

// ❌ 4. 在 Controller 中手动读取请求体
@PostMapping("/create")
public BaseVO create(HttpServletRequest request) throws IOException {
    // 错误:手动读取,无法使用 @RequestBody
    String body = request.getReader().lines()
        .collect(Collectors.joining("\n"));
    UserDTO dto = JSON.parseObject(body, UserDTO.class);
    return R.ok(userService.create(dto));
}

// ✅ 正确做法:使用 @RequestBody
@PostMapping("/create")
public BaseVO create(@RequestBody UserDTO dto) {
    // Spring 自动绑定
    return R.ok(userService.create(dto));
}

// ❌ 5. 禁用 XSS 过滤器来"解决"问题
krismile:
  web:
    xss:
      enabled: false  # 错误:完全禁用不安全

// ✅ 正确做法:配置排除规则
krismile:
  web:
    xss:
      enabled: true

@Bean
public XssFilter.Config xssFilterConfig() {
    return new XssFilter.Config()
        .setExcludeParamEndWith(Arrays.asList("Content"));
}

配置参考

完整配置示例

application.yml

yaml
krismile:
  web:
    # 请求体包装过滤器
    request-body-wrapper:
      enabled: true  # 默认启用
    
    # XSS 攻击防护过滤器
    xss:
      enabled: true  # 默认启用

自定义配置类

java
@Configuration
public class FilterCustomConfig {
    
    /**
     * 自定义请求体包装配置
     */
    @Bean
    public RequestBodyFilter.Config requestBodyFilterConfig() {
        // 默认配置:仅处理 JSON 请求
        return new RequestBodyFilter.Config();
        
        // 自定义配置示例:
        // return new RequestBodyFilter.Config() {
        //     @Override
        //     public Predicate<HttpServletRequest> getExecutePredicate() {
        //         return request -> {
        //             // 自定义判断逻辑
        //             return HttpRequestUtils.isJsonRequest(request);
        //         };
        //     }
        // };
    }
    
    /**
     * 自定义 XSS 过滤配置
     */
    @Bean
    public XssFilter.Config xssFilterConfig() {
        // 创建自定义白名单
        Safelist safelist = new Safelist()
            .addTags("p", "br", "b", "i", "strong", "em")
            .addAttributes("a", "href", "title")
            .addProtocols("a", "href", "http", "https");
        
        return new XssFilter.Config()
            // 执行条件:仅对 /api/ 开头的接口启用
            .setExecutePredicate(request -> 
                request.getRequestURI().startsWith("/api/")
            )
            // 排除富文本字段
            .setExcludeParamEndWith(Arrays.asList(
                "Content",
                "Html",
                "RichText",
                "Description"
            ))
            // 使用自定义白名单
            .setSafeList(safelist)
            // 输出配置
            .setOutputSettings(
                new Document.OutputSettings().prettyPrint(false)
            );
    }
}

环境差异化配置

java
// 开发环境:宽松配置
@Configuration
@Profile("dev")
public class DevFilterConfig {
    
    @Bean
    public XssFilter.Config xssFilterConfig() {
        return new XssFilter.Config()
            .setSafeList(Safelist.relaxed())  // 宽松
            .setExcludeParamEndWith(Arrays.asList(
                "Content", "Html"
            ));
    }
}

// 生产环境:严格配置
@Configuration
@Profile("prod")
public class ProdFilterConfig {
    
    @Bean
    public XssFilter.Config xssFilterConfig() {
        return new XssFilter.Config()
            .setSafeList(Safelist.none())  // 最严格
            .setExcludeParamEndWith(Arrays.asList(
                "articleContent"  // 仅文章内容排除
            ));
    }
}

常见问题

Q: 为什么文件上传失败了?

A: RequestBodyFilter 默认只处理 JSON 请求,不会影响文件上传。如果遇到问题,检查:

java
// 检查是否自定义了执行条件
@Bean
public RequestBodyFilter.Config requestBodyFilterConfig() {
    return new RequestBodyFilter.Config();
    // 默认只处理 JSON 请求,不影响文件上传
}

// 如果自定义了执行条件,确保排除文件上传请求
@Bean
public RequestBodyFilter.Config requestBodyFilterConfig() {
    return new RequestBodyFilter.Config() {
        @Override
        public Predicate<HttpServletRequest> getExecutePredicate() {
            return request -> {
                // ✅ 仅处理 JSON 请求
                return HttpRequestUtils.isJsonRequest(request);
                
                // ❌ 错误:处理所有请求会导致文件上传失败
                // return true;
            };
        }
    };
}

Q: 如何在 AOP 中读取请求体?

A: 框架已经自动启用了 RequestBodyFilter,可以直接读取:

java
@Around("execution(* com.example.controller..*(..))")
public Object logRequest(ProceedingJoinPoint point) throws Throwable {
    HttpServletRequest request = getCurrentRequest();
    
    // ✅ 直接读取(框架已自动包装)
    if (HttpRequestUtils.isJsonRequest(request)) {
        try (BufferedReader reader = request.getReader()) {
            String body = reader.lines().collect(Collectors.joining("\n"));
            log.info("请求体: {}", body);
        }
    }
    
    // Controller 中的 @RequestBody 仍可正常使用
    return point.proceed();
}

Q: XSS 过滤器会影响 JSON 请求体吗?

A: 不会。XssFilter 只过滤以下内容:

  • ✅ 请求参数(request.getParameter()
  • ✅ 请求头(request.getHeader()
  • ❌ 请求体(JSON)- 不过滤

对于 JSON 请求体,建议使用 Bean Validation:

java
public class UserDTO {
    
    @NotBlank
    @Pattern(regexp = "^[a-zA-Z0-9_-]{3,20}$", message = "用户名格式错误")
    private String username;
    
    // 自定义校验器过滤 XSS
    @NoXss  // 自定义注解
    private String description;
}

Q: 如何保留富文本内容的 HTML 标签?

A: 有两种方式:

方式1:配置排除规则(推荐)

java
@Bean
public XssFilter.Config xssFilterConfig() {
    return new XssFilter.Config()
        .setExcludeParamEndWith(Arrays.asList(
            "Content",      // articleContent、richContent 等
            "Html",         // contentHtml、bodyHtml 等
            "RichText"      // richText、richTextContent 等
        ));
}

方式2:使用宽松的 Safelist

java
@Bean
public XssFilter.Config xssFilterConfig() {
    return new XssFilter.Config()
        .setSafeList(Safelist.relaxed());  // 允许大部分 HTML 标签
}

Q: 如何禁用请求体包装或 XSS 过滤?

A: 通过配置文件控制:

yaml
krismile:
  web:
    # 禁用请求体包装
    request-body-wrapper:
      enabled: false
    
    # 禁用 XSS 过滤
    xss:
      enabled: false

Q: RequestBodyFilter 的性能影响?

A: 性能影响很小:

  1. 仅处理 JSON 请求:表单、文件上传不受影响
  2. 内存占用:缓存请求体到字节数组,通常只有几 KB
  3. 执行时间:额外读取一次流,耗时 < 1ms

优化建议

java
// 如果请求体很大(> 10MB),可以限制缓存大小
public class RequestBodyWrapper extends HttpServletRequestWrapper {
    
    private static final int MAX_BODY_SIZE = 10 * 1024 * 1024;  // 10MB
    
    public RequestBodyWrapper(HttpServletRequest request) throws IOException {
        super(request);
        
        int contentLength = request.getContentLength();
        if (contentLength > MAX_BODY_SIZE) {
            throw new IllegalArgumentException("请求体过大");
        }
        
        // ... 读取请求体
    }
}

Q: XssUtils.clean() 的清理规则?

A: 基于 Jsoup 的 Safelist 机制:

java
// 默认:Safelist.none() - 移除所有 HTML 标签
XssUtils.clean("<script>alert()</script>Hello");  
// 输出: "Hello"

XssUtils.clean("<p>Hello</p><b>World</b>");       
// 输出: "HelloWorld"

// 自定义:Safelist.basic() - 允许基本格式标签
XssUtils.clean("<p>Hello</p><script>alert()</script>", Safelist.basic());
// 输出: "<p>Hello</p>"  (保留 <p>,移除 <script>)

完整规则

  • 移除所有不在白名单中的标签
  • 移除标签的不安全属性(如 onclick
  • 移除不安全的协议(如 javascript:
  • 编码特殊字符(如 <, >

Q: 如何自定义 Safelist?

A: 使用链式调用构建:

java
Safelist safelist = new Safelist()
    // 允许的标签
    .addTags("p", "br", "span", "div", "b", "i", "strong", "em")
    
    // 允许的属性
    .addAttributes("a", "href", "title")
    .addAttributes("img", "src", "alt", "width", "height")
    .addAttributes("span", "class", "style")
    
    // 允许的协议
    .addProtocols("a", "href", "http", "https")
    .addProtocols("img", "src", "http", "https")
    
    // 允许的 CSS 属性
    .addEnforcedAttribute("a", "rel", "nofollow");

XssFilter.Config config = new XssFilter.Config()
    .setSafeList(safelist);

💡 提示

  • RequestBodyFilter 解决的是技术问题(流只能读取一次)
  • XssFilter 解决的是安全问题(防护 XSS 攻击)
  • 两者配合使用,提供完整的请求处理能力
  • 生产环境务必启用 XSS 过滤,使用严格的 Safelist

Released under the Apache-2.0 License