fix: 完善工作流公共接口上传与错误契约
- 区分 HTTP 请求标识与内部上传标识,补齐 Redis 旧记录兼容和关联日志 - 保持归一化 MIME 一致,并隔离工作流鉴权错误契约对其他公共接口的影响 - 收口 Multipart 操作日志与对象存储故障分类,归档范围:S05
This commit is contained in:
@@ -6,12 +6,17 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
import org.springframework.web.util.ContentCachingResponseWrapper;
|
||||
import tech.easyflow.common.util.RequestUtil;
|
||||
import tech.easyflow.common.web.error.RequestIdContext;
|
||||
import tech.easyflow.common.web.multipart.MultipartFileMetadataNormalizer;
|
||||
import tech.easyflow.log.annotation.LogReporterDisabled;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
@@ -110,10 +115,16 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
sb.append("EasyFlow action report -------- ").append(timestamp).append(" -------------------------\n");
|
||||
sb.append("Request : ").append(request.getMethod())
|
||||
.append(" ").append(request.getRequestURI()).append("\n");
|
||||
String requestId = RequestIdContext.get(request);
|
||||
if (requestId != null) {
|
||||
sb.append("RequestId : ").append(requestId).append("\n");
|
||||
}
|
||||
|
||||
boolean multipartRequest = isMultipartRequest(request);
|
||||
|
||||
// 打印参数(GET / POST 表单参数),脱敏
|
||||
Map<String, String[]> params = request.getParameterMap();
|
||||
if (!params.isEmpty()) {
|
||||
if (!params.isEmpty() && !multipartRequest) {
|
||||
Map<String, Object> maskedParams = new LinkedHashMap<>();
|
||||
for (Map.Entry<String, String[]> entry : params.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
@@ -127,9 +138,17 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
sb.append("Params : ").append(JSON.toJSONString(maskedParams)).append("\n");
|
||||
}
|
||||
|
||||
if (multipartRequest) {
|
||||
sb.append("Parts : ")
|
||||
.append(buildMultipartSummary(request))
|
||||
.append("\n");
|
||||
}
|
||||
|
||||
// ====== 读取 POST Body ======
|
||||
String methodStr = request.getMethod();
|
||||
if ("POST".equalsIgnoreCase(methodStr) || "PUT".equalsIgnoreCase(methodStr) || "PATCH".equalsIgnoreCase(methodStr)) {
|
||||
if (!multipartRequest && ("POST".equalsIgnoreCase(methodStr)
|
||||
|| "PUT".equalsIgnoreCase(methodStr)
|
||||
|| "PATCH".equalsIgnoreCase(methodStr))) {
|
||||
String body = RequestUtil.readBodyString(request);
|
||||
if (body != null && !body.trim().isEmpty()) {
|
||||
try {
|
||||
@@ -208,8 +227,14 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
if (ex != null) {
|
||||
sb.append('\n')
|
||||
.append("Status : FAILED\n")
|
||||
.append("Exception : ").append(ex.getClass().getSimpleName())
|
||||
.append(": ").append(ex.getMessage() != null ? ex.getMessage().split("\n")[0] : "Unknown");
|
||||
.append("Exception : ")
|
||||
.append(ex.getClass().getSimpleName());
|
||||
if (requestId == null) {
|
||||
sb.append(": ")
|
||||
.append(ex.getMessage() != null
|
||||
? ex.getMessage().split("\n")[0]
|
||||
: "Unknown");
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 耗时 ======
|
||||
@@ -258,6 +283,67 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否为 Multipart 表单。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @return 是否为 Multipart 请求
|
||||
*/
|
||||
private boolean isMultipartRequest(HttpServletRequest request) {
|
||||
String contentType = request.getContentType();
|
||||
return contentType != null
|
||||
&& contentType.regionMatches(
|
||||
true,
|
||||
0,
|
||||
MediaType.MULTIPART_FORM_DATA_VALUE,
|
||||
0,
|
||||
MediaType.MULTIPART_FORM_DATA_VALUE.length());
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建不读取文件正文的 Multipart Part 摘要。
|
||||
*
|
||||
* @param request 当前请求
|
||||
* @return JSON 摘要
|
||||
*/
|
||||
String buildMultipartSummary(HttpServletRequest request) {
|
||||
if (!(request instanceof MultipartHttpServletRequest multipart)) {
|
||||
return "{\"available\":false}";
|
||||
}
|
||||
List<Map<String, Object>> parts = new ArrayList<>();
|
||||
multipart.getMultiFileMap().forEach((partName, files) -> {
|
||||
if (files == null || files.isEmpty()) {
|
||||
parts.add(Map.of("partName", partName, "fileCount", 0));
|
||||
return;
|
||||
}
|
||||
for (MultipartFile file : files) {
|
||||
if (file == null) {
|
||||
parts.add(Map.of("partName", partName, "file", "null"));
|
||||
continue;
|
||||
}
|
||||
String filename =
|
||||
MultipartFileMetadataNormalizer.sanitizeFilename(
|
||||
file.getOriginalFilename());
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("partName", partName);
|
||||
summary.put("fileName", filename);
|
||||
summary.put("size", file.getSize());
|
||||
summary.put(
|
||||
"contentType",
|
||||
MultipartFileMetadataNormalizer.normalizeContentType(
|
||||
filename,
|
||||
file.getContentType()));
|
||||
parts.add(summary);
|
||||
}
|
||||
});
|
||||
Set<String> textPartNames = new LinkedHashSet<>(
|
||||
multipart.getParameterMap().keySet());
|
||||
Map<String, Object> summary = new LinkedHashMap<>();
|
||||
summary.put("files", parts);
|
||||
summary.put("textPartNames", textPartNames);
|
||||
return JSON.toJSONString(summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建方法签名:methodName(paramType paramName, ...)
|
||||
*/
|
||||
@@ -279,4 +365,4 @@ public class ActionReportInterceptor implements HandlerInterceptor {
|
||||
sig.append(")");
|
||||
return sig.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package tech.easyflow.log.reporter;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.junit.Test;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.web.method.HandlerMethod;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.multipart.MultipartHttpServletRequest;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link ActionReportInterceptor} Multipart 日志安全测试。
|
||||
*/
|
||||
public class ActionReportInterceptorTest {
|
||||
|
||||
/**
|
||||
* 验证 Multipart 日志只读取 Part 元数据,不读取原始请求体。
|
||||
*
|
||||
* @throws Exception 构造处理器或日志回调失败
|
||||
*/
|
||||
@Test
|
||||
public void multipartReportShouldNotReadRawRequestBody()
|
||||
throws Exception {
|
||||
ActionReportInterceptor interceptor =
|
||||
new ActionReportInterceptor(
|
||||
new ActionLogReporterProperties());
|
||||
MultipartHttpServletRequest request =
|
||||
mock(MultipartHttpServletRequest.class);
|
||||
HttpServletResponse response = mock(HttpServletResponse.class);
|
||||
MultipartFile file = mock(MultipartFile.class);
|
||||
LinkedMultiValueMap<String, MultipartFile> files =
|
||||
new LinkedMultiValueMap<>();
|
||||
files.add("files.document", file);
|
||||
when(request.getMethod()).thenReturn("POST");
|
||||
when(request.getRequestURI())
|
||||
.thenReturn("/public-api/workflow/runAsync");
|
||||
when(request.getContentType())
|
||||
.thenReturn("multipart/form-data; boundary=test");
|
||||
when(request.getParameterMap()).thenReturn(Map.of());
|
||||
when(request.getMultiFileMap()).thenReturn(files);
|
||||
when(file.getOriginalFilename())
|
||||
.thenReturn("C:\\fakepath\\report.docx");
|
||||
when(file.getContentType()).thenReturn("Other");
|
||||
when(file.getSize()).thenReturn(128L);
|
||||
HandlerMethod handler = new HandlerMethod(
|
||||
new SampleController(),
|
||||
SampleController.class.getDeclaredMethod("run"));
|
||||
|
||||
interceptor.preHandle(request, response, handler);
|
||||
interceptor.afterCompletion(
|
||||
request,
|
||||
response,
|
||||
handler,
|
||||
null);
|
||||
|
||||
verify(request, never()).getInputStream();
|
||||
}
|
||||
|
||||
/**
|
||||
* 提供测试用处理方法。
|
||||
*/
|
||||
private static final class SampleController {
|
||||
|
||||
/**
|
||||
* 测试处理方法。
|
||||
*/
|
||||
private void run() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user