- FlowMapAdminService:previewGroupDepts/importGroups(学校子树 type=7 容器→子组为科室, deptKey=UserBase 组 id,成员=组内用户;已存在启用中映射跳过;语义与 svc-hall buildDeptTree 对齐) - FlowAdminController:GET /map/groupPreview + POST /map/importGroups - flow-center.map.dept-group-type(默认 7) - 含 2026-09-13/14 存量:CallbackAppSigner 回调签名、流程编辑器 sql、示意图(F1-F4)
This commit is contained in:
@@ -67,6 +67,8 @@ class CallbackDispatchIT extends FlowTestSupport {
|
||||
private FlwCallbackRegRepository regRepository;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
@Autowired
|
||||
private com.yunhai.platform.appauth.LocalAppIdentityProvider appIdentity;
|
||||
|
||||
private HttpServer stubServer;
|
||||
private int stubPort;
|
||||
@@ -114,6 +116,10 @@ class CallbackDispatchIT extends FlowTestSupport {
|
||||
String timestamp;
|
||||
String nonce;
|
||||
String signature;
|
||||
/** 应用签名三件套(供 /oapi/* 强制验签的接收方,与 X-Flw-* 并存) */
|
||||
String appKey;
|
||||
String appTimestamp;
|
||||
String appSignature;
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
@@ -130,6 +136,9 @@ class CallbackDispatchIT extends FlowTestSupport {
|
||||
r.timestamp = exchange.getRequestHeaders().getFirst("X-Flw-Timestamp");
|
||||
r.nonce = exchange.getRequestHeaders().getFirst("X-Flw-Nonce");
|
||||
r.signature = exchange.getRequestHeaders().getFirst("X-Flw-Signature");
|
||||
r.appKey = exchange.getRequestHeaders().getFirst("X-App-Key");
|
||||
r.appTimestamp = exchange.getRequestHeaders().getFirst("X-Timestamp");
|
||||
r.appSignature = exchange.getRequestHeaders().getFirst("X-Signature");
|
||||
received.add(r);
|
||||
byte[] resp = "{\"ok\":true}".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, resp.length);
|
||||
@@ -191,15 +200,63 @@ class CallbackDispatchIT extends FlowTestSupport {
|
||||
String expected = hmac(reg.getSecret(), r.timestamp, r.nonce, r.body);
|
||||
assertEquals(expected, r.signature, "推送必须带合法 X-Flw-Signature");
|
||||
assertEquals(r.eventId, payload.get("eventId").asText(), "头与体 eventId 一致");
|
||||
// 应用签名断言:接收方把端点收在 /oapi/* 下时由平台强制验签(未申报 basePath → 签整个 path)
|
||||
assertAppSignature("/callback/flow", r);
|
||||
}
|
||||
assertTrue(hasSubmitted, "应有 flow.submitted 事件");
|
||||
assertTrue(hasTaskCreated, "应有 flow.task.created 事件");
|
||||
// 注册探活 ping 也必须带应用签名(否则接收方 401,RestTemplate 对 4xx 抛异常会误判不可达)
|
||||
Received ping = received.stream()
|
||||
.filter(r -> r.body != null && r.body.contains("\"ping\"")).findFirst().orElse(null);
|
||||
assertNotNull(ping, "注册探活应命中 stub");
|
||||
assertAppSignature("/callback/flow", ping);
|
||||
// 4. 日志表成功态(异步推送落定,轮询等待)
|
||||
assertTrue(logRepository.findAll().size() >= 2);
|
||||
waitUntil(() -> logRepository.findAll().stream()
|
||||
.allMatch(l -> l.getStatus() == FlwEnums.CallbackLogStatus.SUCCESS.getCode()), 10_000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用签名 canonical:注册申报 basePath(接收方 context-path)时,先从 callback_url 的 path 剥离
|
||||
* 再签名——与接收方 AppAuthFilter 用 request.contextPath 剥离后的 path 对齐。
|
||||
* 生产 Notice 部署在 /notice 前缀下(deploy/notice.yaml context-path)即此场景。
|
||||
*/
|
||||
@Test
|
||||
void appSignature_canonicalTargetStripsRegisteredBasePath() {
|
||||
CopyOnWriteArrayList<Received> hits = new CopyOnWriteArrayList<>();
|
||||
stubServer.createContext("/notice/oapi/notice/flow/callback", exchange -> {
|
||||
Received r = new Received();
|
||||
r.body = readBody(exchange);
|
||||
r.appKey = exchange.getRequestHeaders().getFirst("X-App-Key");
|
||||
r.appTimestamp = exchange.getRequestHeaders().getFirst("X-Timestamp");
|
||||
r.appSignature = exchange.getRequestHeaders().getFirst("X-Signature");
|
||||
hits.add(r);
|
||||
byte[] resp = "{\"ok\":true}".getBytes(StandardCharsets.UTF_8);
|
||||
exchange.sendResponseHeaders(200, resp.length);
|
||||
exchange.getResponseBody().write(resp);
|
||||
exchange.close();
|
||||
});
|
||||
mapDept("ybb", 101, "甲");
|
||||
com.yunhai.flowcenter.entity.FlwDef def = saveAndPublishDef("t25", "基路径签名", linearDef("ybb", "审核"));
|
||||
OapiModels.CallbackRegBody rb = regBody(def.getFlowGuid(),
|
||||
"http://localhost:" + stubPort + "/notice/oapi/notice/flow/callback", "flow.submitted");
|
||||
rb.setBasePath("/notice");
|
||||
oapiFlowService.registerCallback(SCH, APP_KEY, rb);
|
||||
|
||||
OapiSignedClient client = new OapiSignedClient("http://localhost:" + port, APP_KEY, APP_SECRET);
|
||||
String body = "{"
|
||||
+ "\"flowGuid\":\"" + def.getFlowGuid() + "\","
|
||||
+ "\"bizType\":\"svc_item\",\"bizId\":\"cb-5\",\"bizTitle\":\"基路径件\","
|
||||
+ "\"submitterId\":201,\"submitterName\":\"张三\"}";
|
||||
client.post("/oapi/flow/apply?schId=1", body, JsonNode.class);
|
||||
|
||||
waitUntil(() -> hits.stream().anyMatch(r -> r.body != null && r.body.contains("flow.submitted")), 10_000);
|
||||
Received push = hits.stream()
|
||||
.filter(r -> r.body != null && r.body.contains("flow.submitted"))
|
||||
.findFirst().orElseThrow(IllegalStateException::new);
|
||||
assertAppSignature("/oapi/notice/flow/callback", push);
|
||||
}
|
||||
|
||||
@Test
|
||||
void pushFailure_persistsRetryBeyondBackoffList() {
|
||||
mapDept("ybb", 101, "甲");
|
||||
@@ -397,6 +454,17 @@ class CallbackDispatchIT extends FlowTestSupport {
|
||||
return body;
|
||||
}
|
||||
|
||||
/** 应用签名断言:canonical = METHOD + "\n" + 剥离 base-path 后的 path(+排序 query) + "\n" + ts */
|
||||
private void assertAppSignature(String expectedTarget, Received r) {
|
||||
assertNotNull(r.appKey, "推送必须带 X-App-Key");
|
||||
assertEquals(appIdentity.getAppKey(), r.appKey, "签名身份应是本应用 appKey");
|
||||
assertNotNull(r.appTimestamp, "推送必须带 X-Timestamp");
|
||||
assertNotNull(r.appSignature, "推送必须带 X-Signature");
|
||||
assertTrue(com.yunhai.platform.utils.crypto.SignUtil.verifyRequest(
|
||||
"POST", expectedTarget, r.appTimestamp, appIdentity.getAppSecret(), r.appSignature),
|
||||
"应用签名应按 target=" + expectedTarget + " 可验(与接收方 AppAuthFilter 剥离 context-path 后对齐)");
|
||||
}
|
||||
|
||||
/** 轮询等待(替代 Awaitility,离线环境无该依赖) */
|
||||
private static void waitUntil(java.util.function.BooleanSupplier cond, long timeoutMs) {
|
||||
long deadline = System.currentTimeMillis() + timeoutMs;
|
||||
|
||||
Reference in New Issue
Block a user