You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot中ContentCachingRequestWrapper请求体日志的优化问题

解决Spring控制器审计的三个核心问题

针对你遇到的请求重复反序列化、控制器依赖过滤器、无法使用@RequestBody的问题,推荐以下两种实用方案,同时满足审计记录、MDC设置和敏感字段忽略的需求:


方案一:过滤器 + RequestBodyAdvice + ResponseBodyAdvice(推荐,完全解耦)

这种方案利用Spring原生组件实现请求/响应缓存、MDC设置和审计,彻底解决三个核心问题。

1. 全局审计过滤器(仅做缓存,不解析请求体)

只负责包装请求和响应,缓存内容用于后续审计,不做任何业务解析,避免重复反序列化:

@Component
public class AuditFilter extends OncePerRequestFilter {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 包装请求/响应,自动缓存请求体和响应体
        ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper cachedResponse = new ContentCachingResponseWrapper(response);

        try {
            // 先执行控制器逻辑
            filterChain.doFilter(cachedRequest, cachedResponse);
        } finally {
            // 记录审计日志,忽略敏感字段
            logAudit(cachedRequest, cachedResponse);
            // 必须调用此方法,否则响应体无法返回给客户端
            cachedResponse.copyBodyToResponse();
            // 清除MDC,避免线程复用污染
            MDC.clear();
        }
    }

    private void logAudit(ContentCachingRequestWrapper request, ContentCachingResponseWrapper response) {
        String requestBody = new String(request.getContentAsByteArray(), request.getCharacterEncoding());
        // 替换敏感字段(比如password)
        requestBody = maskSensitiveField(requestBody, "password");
        
        String responseBody = new String(response.getContentAsByteArray(), response.getCharacterEncoding());
        
        // 输出审计信息(可替换为日志框架)
        System.out.printf("URI: %s, Method: %s, Request: %s, Response: %s, Status: %d%n",
                request.getRequestURI(), request.getMethod(), requestBody, responseBody, response.getStatus());
    }

    private String maskSensitiveField(String json, String fieldName) {
        try {
            JsonNode node = objectMapper.readTree(json);
            if (node.has(fieldName)) {
                ((ObjectNode) node).put(fieldName, "***");
                return objectMapper.writeValueAsString(node);
            }
        } catch (JsonProcessingException e) {
            // 解析失败时返回原始内容
            return json;
        }
        return json;
    }
}

2. RequestBodyAdvice设置MDC(复用Spring的反序列化结果)

在Spring MVC解析@RequestBody的同时,直接从已解析的对象中提取信息设置MDC,无需重复解析请求体:

@ControllerAdvice
public class MdcRequestBodyAdvice implements RequestBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        // 仅对带有@RequestBody的参数生效
        return methodParameter.hasParameterAnnotation(RequestBody.class);
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return inputMessage;
    }

    @Override
    public Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        // 从解析后的对象中提取MDC所需字段
        if (body instanceof MyBean) {
            MyBean myBean = (MyBean) body;
            MDC.put("userId", myBean.getUserId());
            MDC.put("requestId", myBean.getRequestId());
        }
        return body;
    }

    @Override
    public Object handleEmptyBody(Object body, HttpInputMessage inputMessage, MethodParameter parameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        return body;
    }
}

3. 控制器正常使用@RequestBody

无需任何特殊处理,直接使用标准Spring MVC语法,完全不感知过滤器的存在:

@RestController
@RequestMapping("/api")
public class MyController {

    @PostMapping("/test")
    public MyResponse test(@RequestBody MyBean myBean) {
        // 业务逻辑中可直接使用MDC中的属性
        System.out.println("当前请求用户ID: " + MDC.get("userId"));
        return new MyResponse("success");
    }
}

方案优势:

  • 无重复反序列化:请求体仅被Spring MVC解析一次,Advice直接复用结果
  • 控制器无依赖:完全使用标准@RequestBody,无需感知过滤器或包装类
  • MDC设置时机正确:在控制器处理业务前完成设置,不影响业务逻辑

方案二:优化过滤器实现(轻量方案)

如果不想使用Advice,可优化过滤器逻辑,避免重复解析同时兼容@RequestBody:

@Component
public class AuditFilter extends OncePerRequestFilter {

    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        ContentCachingRequestWrapper cachedRequest = new ContentCachingRequestWrapper(request);
        ContentCachingResponseWrapper cachedResponse = new ContentCachingResponseWrapper(response);

        try {
            // 仅解析一次请求体,设置MDC并存入请求属性
            byte[] requestBytes = cachedRequest.getContentAsByteArray();
            if (requestBytes.length > 0 && MediaType.APPLICATION_JSON_VALUE.equals(request.getContentType())) {
                MyBean myBean = objectMapper.readValue(requestBytes, MyBean.class);
                MDC.put("userId", myBean.getUserId());
                request.setAttribute("parsedMyBean", myBean);
            }

            // Spring MVC会从缓存的请求体中读取,不会重复解析
            filterChain.doFilter(cachedRequest, cachedResponse);
        } finally {
            // 审计日志处理同方案一
            String requestBody = maskSensitiveField(new String(cachedRequest.getContentAsByteArray(), cachedRequest.getCharacterEncoding()), "password");
            String responseBody = new String(cachedResponse.getContentAsByteArray(), cachedResponse.getCharacterEncoding());
            System.out.printf("URI: %s, Request: %s, Response: %s%n",
                    cachedRequest.getRequestURI(), requestBody, responseBody);
            cachedResponse.copyBodyToResponse();
            MDC.clear();
        }
    }

    private String maskSensitiveField(String json, String fieldName) {
        // 同方案一实现
        try {
            JsonNode node = objectMapper.readTree(json);
            if (node.has(fieldName)) {
                ((ObjectNode) node).put(fieldName, "***");
                return objectMapper.writeValueAsString(node);
            }
        } catch (JsonProcessingException e) {
            return json;
        }
        return json;
    }
}

控制器可选使用方式:

  1. 继续使用@RequestBody(Spring MVC会从缓存读取,性能影响极小)
  2. 直接从请求属性获取已解析的对象,彻底避免二次解析:
@PostMapping("/test")
public MyResponse test(HttpServletRequest request) {
    MyBean myBean = (MyBean) request.getAttribute("parsedMyBean");
    // 业务逻辑处理
    return new MyResponse("success");
}

关键注意事项

  • 使用ContentCachingRequestWrapper时,无需手动重置流,它会自动缓存并允许重复读取
  • 响应记录必须调用copyBodyToResponse(),否则客户端无法收到响应体
  • MDC必须在finally块中清除,防止线程池复用导致的上下文污染

内容的提问来源于stack exchange,提问作者user15110545

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 23:42:22