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; } }
控制器可选使用方式:
- 继续使用
@RequestBody(Spring MVC会从缓存读取,性能影响极小) - 直接从请求属性获取已解析的对象,彻底避免二次解析:
@PostMapping("/test") public MyResponse test(HttpServletRequest request) { MyBean myBean = (MyBean) request.getAttribute("parsedMyBean"); // 业务逻辑处理 return new MyResponse("success"); }
关键注意事项
- 使用
ContentCachingRequestWrapper时,无需手动重置流,它会自动缓存并允许重复读取 - 响应记录必须调用
copyBodyToResponse(),否则客户端无法收到响应体 - MDC必须在finally块中清除,防止线程池复用导致的上下文污染
内容的提问来源于stack exchange,提问作者user15110545
相关产品推荐
相关产品推荐

