From 5f19cdcccb36e13cc59f9e752121af639d9706b1 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Sat, 8 Aug 2026 10:18:29 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../OpenApiInboundMeasurementController.java | 4 +-- .../main/resources/openapi/firstleg-v1.yaml | 6 +++- ...oundMeasurementControllerContractTest.java | 6 ++++ .../MeasurementRequestAuthenticatorTest.java | 30 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/controller/OpenApiInboundMeasurementController.java b/firstleg/src/main/java/com/baiyue/firstleg/controller/OpenApiInboundMeasurementController.java index a89af6f6c..15f81642f 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/controller/OpenApiInboundMeasurementController.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/controller/OpenApiInboundMeasurementController.java @@ -38,7 +38,7 @@ public class OpenApiInboundMeasurementController { private InboundMeasurementOpenApiService service; @Operation(summary = "提交一条第三方箱规测量结果和图片", - description = "multipart 包含 data 原始JSON和 image 图片;签名 body 为 data原文、换行、图片SHA-256,外层仍按 METHOD、PATH、时间戳、Nonce、Idempotency-Key、body SHA-256 六行签名") + description = "multipart 包含 data 原始JSON和 image 图片;联调关闭签名时只需固定 X-App-Key 和 Idempotency-Key,恢复后按六行 HMAC 规范签名") @ApiResponses({ @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "成功或业务失败,按响应体 code 判断"), @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "413", description = "请求体超过网关限制"), @@ -59,7 +59,7 @@ public class OpenApiInboundMeasurementController { } @Operation(summary = "查询第三方测量事件状态", - description = "GET 签名的 Idempotency-Key 行和 body SHA-256 分别为空字符串、空字节 SHA-256") + description = "联调关闭签名时只需固定 X-App-Key;恢复后 GET 签名的 Idempotency-Key 行和 body SHA-256 分别为空字符串、空字节 SHA-256") @ApiResponses(@io.swagger.v3.oas.annotations.responses.ApiResponse( responseCode = "200", description = "查询成功或业务失败,按响应体 code 判断")) @GetMapping("/events/{eventNo}") diff --git a/firstleg/src/main/resources/openapi/firstleg-v1.yaml b/firstleg/src/main/resources/openapi/firstleg-v1.yaml index d7a4e0319..ad188e8cb 100644 --- a/firstleg/src/main/resources/openapi/firstleg-v1.yaml +++ b/firstleg/src/main/resources/openapi/firstleg-v1.yaml @@ -215,6 +215,8 @@ paths: description: >- data 中的 inboundCode 匹配入库单,inboundDetailId 必须匹配该入库单关联订单中的已有货物明细; 每次只保存一条箱规明细和一张图片,不审核、不计费、不入库,也不改变入库单状态。 + 首轮联调可由服务端配置 signature-required=false:此时只强制固定 X-App-Key 和 + Idempotency-Key,X-Timestamp、X-Nonce、X-Signature 可不传;恢复签名后按下述规则计算。 签名串依次为 METHOD、原始 PATH、X-Timestamp、X-Nonce、Idempotency-Key、 签名body的 SHA-256 小写十六进制,六部分使用 LF(\n)连接且末尾不追加换行。 签名body = data原始UTF-8文本 + LF + image文件内容SHA-256小写十六进制。 @@ -253,7 +255,9 @@ paths: /openapi/v1/inbound-measurements/events/{eventNo}: get: summary: 查询第三方测量事件状态 - description: GET 签名串的 Idempotency-Key 行为空字符串,body 摘要为空字节 SHA-256。 + description: >- + signature-required=false 时只需固定 X-App-Key;恢复签名后,GET 签名串的 + Idempotency-Key 行为空字符串,body 摘要为空字节 SHA-256。 security: - measurementAppKey: [] measurementTimestamp: [] diff --git a/firstleg/src/test/java/com/baiyue/firstleg/controller/OpenApiInboundMeasurementControllerContractTest.java b/firstleg/src/test/java/com/baiyue/firstleg/controller/OpenApiInboundMeasurementControllerContractTest.java index 8a5f0ac5f..a2cfd524a 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/controller/OpenApiInboundMeasurementControllerContractTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/controller/OpenApiInboundMeasurementControllerContractTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.core.io.ClassPathResource; import org.springframework.web.bind.annotation.RequestPart; +import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.multipart.MultipartFile; import java.lang.reflect.Method; @@ -22,6 +23,11 @@ class OpenApiInboundMeasurementControllerContractTest { assertThat(method.getParameters()[5].getAnnotation(RequestPart.class).value()).isEqualTo("data"); assertThat(method.getParameters()[5].getType()).isEqualTo(String.class); assertThat(method.getParameters()[6].getAnnotation(RequestPart.class).value()).isEqualTo("image"); + assertThat(method.getParameters()[0].getAnnotation(RequestHeader.class).required()).isTrue(); + assertThat(method.getParameters()[1].getAnnotation(RequestHeader.class).required()).isFalse(); + assertThat(method.getParameters()[2].getAnnotation(RequestHeader.class).required()).isFalse(); + assertThat(method.getParameters()[3].getAnnotation(RequestHeader.class).required()).isFalse(); + assertThat(method.getParameters()[4].getAnnotation(RequestHeader.class).required()).isTrue(); } @Test diff --git a/firstleg/src/test/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticatorTest.java b/firstleg/src/test/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticatorTest.java index dcd8fd0db..da3b39cc3 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticatorTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/security/MeasurementRequestAuthenticatorTest.java @@ -21,6 +21,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verifyNoInteractions; @DisplayName("第三方测量请求签名校验") @SpringJUnitConfig(classes = {MeasurementRequestAuthenticator.class, @@ -35,6 +36,7 @@ class MeasurementRequestAuthenticatorTest { @BeforeEach void setUp() { properties.setEnabled(true); + properties.setSignatureRequired(true); MeasurementOpenApiProperties.AppCredential credential = new MeasurementOpenApiProperties.AppCredential(); credential.setSecret(SECRET); credential.setTenantId(10001L); @@ -99,6 +101,34 @@ class MeasurementRequestAuthenticatorTest { .isEqualTo(ResultCode.CONFLICT)); } + @Test + @DisplayName("联调关闭签名时只用固定AppKey和幂等键认证且不占用Nonce") + void shouldAcceptFixedAppKeyWithoutSignatureDuringIntegration() { + properties.setSignatureRequired(false); + properties.getApps().get("device-provider").setSecret(null); + + AuthenticatedMeasurementApp app = authenticator.authenticate("POST", + "/openapi/v1/inbound-measurements", + new MeasurementRequestHeaders("device-provider", null, null, null, "idem-1"), + "{}", true); + + assertThat(app.tenantId()).isEqualTo(10001L); + assertThat(app.subjectId()).isEqualTo(90001L); + verifyNoInteractions(guard); + } + + @Test + @DisplayName("关闭签名仍强制上传请求携带幂等键") + void shouldStillRequireIdempotencyKeyWithoutSignature() { + properties.setSignatureRequired(false); + + assertThatThrownBy(() -> authenticator.authenticate("POST", + "/openapi/v1/inbound-measurements", + new MeasurementRequestHeaders("device-provider", null, null, null, null), "{}", true)) + .isInstanceOf(BizException.class) + .hasMessageContaining("Idempotency-Key"); + } + @Configuration static class TestConfig { @Bean