fix log multipart serialization

This commit is contained in:
PIGCLOUD committed 2026-08-01 11:59:35 +08:00
1 parent 2bc5fb5f24
commit 0a15577761
2 files changed
+124 -6

No files matched your search

@@ -24,15 +24,25 @@ import com.pig4cloud.pigx.common.core.jackson.PigxJavaTimeModule;
import com.pig4cloud.pigx.common.log.config.PigxLogProperties;
import com.pig4cloud.pigx.common.log.util.JacksonSensitiveFieldUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Part;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.event.EventListener;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.Resource;
import org.springframework.scheduling.annotation.Async;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
@@ -47,11 +57,9 @@ public class SysLogListener implements InitializingBean {
/**
* 忽略序列化的对象类型
*/
private final static Class[] ignoreClass = {ServletRequest.class, BindingResult.class};
/**
* new 一个 避免日志脱敏策略影响全局ObjectMapper
*/
private static final Class<?>[] IGNORE_CLASSES = {ServletRequest.class, ServletResponse.class, BindingResult.class,
MultipartFile.class, Part.class, InputStream.class, OutputStream.class, Reader.class, Writer.class,
Resource.class, File.class, Path.class};
private final RemoteLogService remoteLogService;
@@ -69,7 +77,7 @@ public class SysLogListener implements InitializingBean {
Object[] args = (Object[]) source.getBody();
List<Object> list = CollUtil.toList(args);
// 删除部分无法序列化的参数
list.removeIf(obj -> Arrays.stream(ignoreClass).anyMatch(clazz -> clazz.isAssignableFrom(obj.getClass())));
list.removeIf(this::isIgnoredParameter);
try {
// 序列化参数
@@ -84,6 +92,19 @@ public class SysLogListener implements InitializingBean {
remoteLogService.saveLog(source);
}
/**
* 判断请求参数是否属于不可异步序列化的类型
* <p>
* 请求结束后 Tomcat 会清理 multipart 临时文件,流、文件类参数在异步线程中序列化会触发
* NoSuchFileException,需要在序列化前剔除。
* @param obj 请求参数,可能为 null(null 可正常序列化,予以保留)
* @return true 表示需要从序列化列表中剔除
*/
private boolean isIgnoredParameter(Object obj) {
return Objects.nonNull(obj)
&& Arrays.stream(IGNORE_CLASSES).anyMatch(clazz -> clazz.isAssignableFrom(obj.getClass()));
}
@Override
public void afterPropertiesSet() {
String[] ignorableFieldNames = logProperties.getExcludeFields().toArray(new String[0]);
@@ -0,0 +1,97 @@
package com.pig4cloud.pigx.common.log.event;
import com.pig4cloud.pigx.admin.api.dto.SysLogDTO;
import com.pig4cloud.pigx.admin.api.feign.RemoteLogService;
import com.pig4cloud.pigx.common.core.util.R;
import com.pig4cloud.pigx.common.log.config.PigxLogProperties;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* {@link SysLogListener} 请求参数过滤测试
*
* @author lengleng
*/
class SysLogListenerTests {
/**
* MultipartFile 参数必须在序列化前被剔除,且日志仍正常保存
*/
@Test
void filtersMultipartFileBeforeSerializingRequestParams() {
RemoteLogService remoteLogService = mock(RemoteLogService.class);
when(remoteLogService.saveLog(any(SysLogDTO.class))).thenReturn(R.ok(Boolean.TRUE));
PigxLogProperties logProperties = new PigxLogProperties();
logProperties.setExcludeFields(List.of());
SysLogListener listener = new SysLogListener(remoteLogService, logProperties);
listener.afterPropertiesSet();
SysLogDTO sysLog = new SysLogDTO();
sysLog.setBody(new Object[] { new ThrowingMultipartFile() });
listener.saveSysLog(new SysLogEvent(sysLog));
ArgumentCaptor<SysLogDTO> captor = ArgumentCaptor.forClass(SysLogDTO.class);
verify(remoteLogService).saveLog(captor.capture());
assertThat(captor.getValue().getParams()).isEqualTo("[]");
assertThat(captor.getValue().getBody()).isNull();
}
private static class ThrowingMultipartFile implements MultipartFile {
@Override
public String getName() {
throw new AssertionError("MultipartFile should be filtered before serialization");
}
@Override
public String getOriginalFilename() {
throw new AssertionError("MultipartFile should be filtered before serialization");
}
@Override
public String getContentType() {
throw new AssertionError("MultipartFile should be filtered before serialization");
}
@Override
public boolean isEmpty() {
throw new AssertionError("MultipartFile should be filtered before serialization");
}
@Override
public long getSize() {
throw new AssertionError("MultipartFile should be filtered before serialization");
}
@Override
public byte[] getBytes() throws IOException {
throw new AssertionError("MultipartFile should be filtered before serialization");
}
@Override
public InputStream getInputStream() throws IOException {
throw new AssertionError("MultipartFile should be filtered before serialization");
}
@Override
public void transferTo(File dest) throws IOException, IllegalStateException {
throw new AssertionError("MultipartFile should be filtered before serialization");
}
}
}