Appearance
价态增量回调接口
简要描述
- 酒店价态增量回调接口
请求URL
/Hotel/HotelRoomRateIncrement
请求方式
- POST
参数
联系业务配置回调基础url
验签方法
java
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public final class DistributorSignatureUtil {
private static final String SIGN_TYPE = "HMAC-SHA256";
private static final String HMAC_ALGORITHM = "HmacSHA256";
private static final String SHA256_ALGORITHM = "SHA-256";
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
private DistributorSignatureUtil() {
}
/**
* 验证分销商推送签名。
*
* @param payload 请求JSON
* @param secret PartnerId对应的共享密钥
* @return true表示验签成功
*/
public static boolean verifyDistributorPayload(
JsonNode payload,
String secret
) {
if (payload == null || !payload.isObject()) {
return false;
}
String sign = getText(payload, "Sign");
String signType = getText(payload, "SignType");
if (secret == null
|| secret.isEmpty()
|| sign.isEmpty()
|| !SIGN_TYPE.equals(signType)
|| !sign.matches("(?i)^[a-f0-9]{64}$")) {
return false;
}
try {
JsonNode data = payload.has("Data")
? payload.get("Data")
: OBJECT_MAPPER.createObjectNode();
JsonNode normalizedData = normalizeDistributorSignPayload(data);
// Jackson默认不会转义中文和斜杠,数组顺序保持不变。
String dataJson = OBJECT_MAPPER.writeValueAsString(normalizedData);
String dataSha256 = sha256Hex(dataJson);
String signString = String.join(
"\n",
"PartnerId=" + getText(payload, "PartnerId"),
"Event=" + getText(payload, "Event"),
"Guid=" + getText(payload, "guid"),
"SignType=" + signType,
"DataSha256=" + dataSha256
);
String expectedSign = hmacSha256Hex(signString, secret);
return MessageDigest.isEqual(
expectedSign.getBytes(StandardCharsets.US_ASCII),
sign.toLowerCase(Locale.ROOT)
.getBytes(StandardCharsets.US_ASCII)
);
} catch (Exception exception) {
return false;
}
}
/**
* 递归规范化JSON:
* 1. 对象字段按字段名升序排序;
* 2. 数组保持原始顺序;
* 3. 基础类型保持不变。
*/
private static JsonNode normalizeDistributorSignPayload(JsonNode value) {
if (value == null || value.isNull()) {
return OBJECT_MAPPER.nullNode();
}
if (value.isArray()) {
ArrayNode normalizedArray = OBJECT_MAPPER.createArrayNode();
for (JsonNode item : value) {
normalizedArray.add(normalizeDistributorSignPayload(item));
}
return normalizedArray;
}
if (value.isObject()) {
ObjectNode normalizedObject = OBJECT_MAPPER.createObjectNode();
List<String> fieldNames = new ArrayList<>();
Iterator<Map.Entry<String, JsonNode>> fields = value.fields();
while (fields.hasNext()) {
fieldNames.add(fields.next().getKey());
}
Collections.sort(fieldNames);
for (String fieldName : fieldNames) {
normalizedObject.set(
fieldName,
normalizeDistributorSignPayload(value.get(fieldName))
);
}
return normalizedObject;
}
return value.deepCopy();
}
private static String sha256Hex(String content) throws Exception {
MessageDigest digest = MessageDigest.getInstance(SHA256_ALGORITHM);
byte[] hash = digest.digest(
content.getBytes(StandardCharsets.UTF_8)
);
return toHex(hash);
}
private static String hmacSha256Hex(
String content,
String secret
) throws Exception {
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
SecretKeySpec secretKey = new SecretKeySpec(
secret.getBytes(StandardCharsets.UTF_8),
HMAC_ALGORITHM
);
mac.init(secretKey);
byte[] hash = mac.doFinal(
content.getBytes(StandardCharsets.UTF_8)
);
return toHex(hash);
}
private static String toHex(byte[] bytes) {
StringBuilder result = new StringBuilder(bytes.length * 2);
for (byte value : bytes) {
result.append(String.format("%02x", value & 0xff));
}
return result.toString();
}
private static String getText(JsonNode payload, String fieldName) {
JsonNode value = payload.get(fieldName);
if (value == null || value.isNull()) {
return "";
}
return value.isTextual() ? value.textValue() : value.asText("");
}
}请求示例
{
"PartnerId": "1234567",
"Event": "HotelRoomRateIncrement",
"Data": {
"HotelIds": [
185768,
319076,
705368,
765569,
957543,
1120517,
1249401,
1392241,
1509236,
1857941,
2012950,
2031026,
2793712,
2917578
],
"CurrentTimestamp": 1788847866
},
"guid": "b5c532d2c7c5f36eb6a954b9fae3c591",
"SignType": "HMAC-SHA256",
"Sign": "402fd68f53be82e21ca2621ac766d2b764b3ad38c9ae800c9cf6a2a6ff527632"
}响应参数(返回的Code请默认返回成功值200)
{
"Data":{},
"Msg":"",
"Code":200
}返回参数说明
| 参数名 | 类型 | 说明 |
|---|---|---|
| HotelIds | array | 奈斯酒店ID列表 |
| PartnerId | string | 分销商ID |
| Event | string | 事件名 |
| Sign | string | 签名 |