This commit is contained in:
2026-08-08 10:18:29 +08:00
parent 4f48015b81
commit 5f19cdcccb
4 changed files with 43 additions and 3 deletions
@@ -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}")
@@ -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: []
@@ -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
@@ -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