优化
This commit is contained in:
+2
-2
@@ -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: []
|
||||
|
||||
+6
@@ -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
|
||||
|
||||
+30
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user