fix: 完善工作流公共接口上传与错误契约
- 区分 HTTP 请求标识与内部上传标识,补齐 Redis 旧记录兼容和关联日志 - 保持归一化 MIME 一致,并隔离工作流鉴权错误契约对其他公共接口的影响 - 收口 Multipart 操作日志与对象存储故障分类,归档范围:S05
This commit is contained in:
@@ -32,6 +32,7 @@ import tech.easyflow.common.constant.Constants;
|
||||
import tech.easyflow.common.domain.Result;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.satoken.util.SaTokenUtil;
|
||||
import tech.easyflow.common.web.error.RequestIdContext;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.common.web.jsonbody.JsonBody;
|
||||
import tech.easyflow.publicapi.dto.PublicWorkflowInfo;
|
||||
@@ -194,6 +195,7 @@ public class PublicWorkflowController {
|
||||
multipartRequest.getMultiFileMap());
|
||||
WorkflowApiPreparedUpload preparedUpload =
|
||||
workflowApiUploadLifecycleService.prepare(
|
||||
RequestIdContext.get(request),
|
||||
workflow.getContent(),
|
||||
metadata.getVariables(),
|
||||
fileParts);
|
||||
@@ -205,12 +207,12 @@ public class PublicWorkflowController {
|
||||
topology,
|
||||
executeId ->
|
||||
workflowApiUploadLifecycleService.bindExecution(
|
||||
preparedUpload.getRequestId(),
|
||||
preparedUpload.getUploadId(),
|
||||
executeId));
|
||||
} catch (RuntimeException | Error error) {
|
||||
try {
|
||||
workflowApiUploadLifecycleService.abort(
|
||||
preparedUpload.getRequestId());
|
||||
preparedUpload.getUploadId());
|
||||
} catch (RuntimeException cleanupError) {
|
||||
error.addSuppressed(cleanupError);
|
||||
}
|
||||
|
||||
@@ -40,6 +40,12 @@ public class PublicApiInterceptor implements HandlerInterceptor {
|
||||
String apiKey = request.getHeader("ApiKey");
|
||||
|
||||
if (apiKey == null || apiKey.isBlank()) {
|
||||
if (!isWorkflowApi(requestURI)) {
|
||||
Result<Void> failed = Result.fail(401, "密钥不正确");
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
ResponseUtil.renderJson(response, failed);
|
||||
return false;
|
||||
}
|
||||
Result<PublicApiErrorDetail> failed = Result.fail(
|
||||
"缺少 ApiKey 请求头",
|
||||
new PublicApiErrorDetail(
|
||||
@@ -62,4 +68,15 @@ public class PublicApiInterceptor implements HandlerInterceptor {
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为工作流公共 API,避免专项错误契约影响其他公共接口。
|
||||
*
|
||||
* @param requestUri 请求 URI
|
||||
* @return 是否为工作流公共 API
|
||||
*/
|
||||
private boolean isWorkflowApi(String requestUri) {
|
||||
return requestUri != null
|
||||
&& requestUri.contains("/public-api/workflow/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@ public class PublicWorkflowControllerRoutingTest {
|
||||
when(multipartMapper.map(any()))
|
||||
.thenReturn(Map.of("file", List.of()));
|
||||
when(uploadLifecycleService.prepare(
|
||||
anyString(),
|
||||
eq("{}"),
|
||||
anyMap(),
|
||||
anyMap()))
|
||||
@@ -181,6 +182,7 @@ public class PublicWorkflowControllerRoutingTest {
|
||||
eq("{}"),
|
||||
anyMap());
|
||||
verify(uploadLifecycleService, never()).prepare(
|
||||
anyString(),
|
||||
anyString(),
|
||||
anyMap(),
|
||||
anyMap());
|
||||
@@ -209,12 +211,14 @@ public class PublicWorkflowControllerRoutingTest {
|
||||
mockMvc.perform(multipart(RUN_PATH)
|
||||
.file(metadata)
|
||||
.file(file)
|
||||
.header("ApiKey", "key"))
|
||||
.header("ApiKey", "key")
|
||||
.header("X-Request-Id", "request-multipart"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data").value("execute-1"));
|
||||
|
||||
verify(multipartMapper).map(any());
|
||||
verify(uploadLifecycleService).prepare(
|
||||
eq("request-multipart"),
|
||||
eq("{}"),
|
||||
anyMap(),
|
||||
anyMap());
|
||||
|
||||
@@ -74,6 +74,59 @@ public class PublicApiInterceptorTest {
|
||||
Assert.assertTrue(body.toString().contains("request-1"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证非工作流公共 API 缺少令牌时继续保留既有通用错误契约。
|
||||
*
|
||||
* @throws Exception 拦截器处理失败时抛出
|
||||
*/
|
||||
@Test
|
||||
public void shouldKeepGenericMissingKeyContractForOtherPublicApis()
|
||||
throws Exception {
|
||||
StringWriter body = new StringWriter();
|
||||
AtomicInteger status = new AtomicInteger();
|
||||
HttpServletRequest request = proxy(
|
||||
HttpServletRequest.class,
|
||||
(instance, method, args) -> {
|
||||
if ("getRequestURI".equals(method.getName())) {
|
||||
return "/public-api/knowledge-share/detail";
|
||||
}
|
||||
if ("getHeader".equals(method.getName())) {
|
||||
return null;
|
||||
}
|
||||
throw new AssertionError(
|
||||
"测试路径不应调用 HttpServletRequest."
|
||||
+ method.getName());
|
||||
});
|
||||
HttpServletResponse response = proxy(
|
||||
HttpServletResponse.class,
|
||||
(instance, method, args) -> {
|
||||
if ("setStatus".equals(method.getName())) {
|
||||
status.set((Integer) args[0]);
|
||||
return null;
|
||||
}
|
||||
if ("setContentType".equals(method.getName())) {
|
||||
return null;
|
||||
}
|
||||
if ("getWriter".equals(method.getName())) {
|
||||
return new PrintWriter(body);
|
||||
}
|
||||
throw new AssertionError(
|
||||
"测试路径不应调用 HttpServletResponse."
|
||||
+ method.getName());
|
||||
});
|
||||
|
||||
boolean allowed = new PublicApiInterceptor()
|
||||
.preHandle(request, response, new Object());
|
||||
|
||||
Assert.assertFalse(allowed);
|
||||
Assert.assertEquals(
|
||||
HttpServletResponse.SC_UNAUTHORIZED,
|
||||
status.get());
|
||||
Assert.assertTrue(body.toString().contains("\"errorCode\":401"));
|
||||
Assert.assertTrue(body.toString().contains("密钥不正确"));
|
||||
Assert.assertFalse(body.toString().contains("工作流 Public API Key"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证通过接口权限校验的访问令牌会写入请求,供资源级鉴权复用。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user