|
@@ -108,9 +108,9 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
|
|
|
private com.haha.service.OrderInventoryService orderInventoryService;
|
|
private com.haha.service.OrderInventoryService orderInventoryService;
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
- public void handleMessage(Map<String, Object> params) {
|
|
|
|
|
|
|
+ public void handleMessage(Map<String, Object> params, String rawBody) {
|
|
|
// 签名验证:确保回调来自哈哈平台
|
|
// 签名验证:确保回调来自哈哈平台
|
|
|
- if (!validateSign(params)) {
|
|
|
|
|
|
|
+ if (!validateSign(params, rawBody)) {
|
|
|
log.warn("消息回调签名验证失败,忽略该通知");
|
|
log.warn("消息回调签名验证失败,忽略该通知");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -552,9 +552,9 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
- public void handleOrderCallback(Map<String, Object> params) {
|
|
|
|
|
|
|
+ public void handleOrderCallback(Map<String, Object> params, String rawBody) {
|
|
|
// 签名验证:确保订单回调来自哈哈平台
|
|
// 签名验证:确保订单回调来自哈哈平台
|
|
|
- if (!validateSign(params)) {
|
|
|
|
|
|
|
+ if (!validateSign(params, rawBody)) {
|
|
|
log.warn("订单回调签名验证失败,忽略该通知");
|
|
log.warn("订单回调签名验证失败,忽略该通知");
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
@@ -614,6 +614,11 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
|
|
|
|
|
|
|
|
@Override
|
|
@Override
|
|
|
public boolean validateSign(Map<String, Object> params) {
|
|
public boolean validateSign(Map<String, Object> params) {
|
|
|
|
|
+ return validateSign(params, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public boolean validateSign(Map<String, Object> params, String rawBody) {
|
|
|
String receivedSign = extractParam(params, "sign");
|
|
String receivedSign = extractParam(params, "sign");
|
|
|
if (receivedSign == null || receivedSign.isEmpty()) {
|
|
if (receivedSign == null || receivedSign.isEmpty()) {
|
|
|
log.warn("签名参数为空,回调来源可能未经验证");
|
|
log.warn("签名参数为空,回调来源可能未经验证");
|
|
@@ -653,7 +658,14 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
|
|
|
// 第一步:参数按键排序,值 URL 编码后拼接为 k1=v1&k2=v2&...
|
|
// 第一步:参数按键排序,值 URL 编码后拼接为 k1=v1&k2=v2&...
|
|
|
// 第二步:末尾直接拼接 ticket(无 & 或 = 分隔符)
|
|
// 第二步:末尾直接拼接 ticket(无 & 或 = 分隔符)
|
|
|
// 第三步:MD5 得到签名
|
|
// 第三步:MD5 得到签名
|
|
|
- String signContent = buildSignContent(params);
|
|
|
|
|
|
|
+ String signContent;
|
|
|
|
|
+ if (rawBody != null && !rawBody.isEmpty()) {
|
|
|
|
|
+ // 优先:从原始请求体构建签名,保留 PHP 端的原始 URL 编码
|
|
|
|
|
+ signContent = buildSignContentFromRawBody(rawBody);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // fallback:从已解析的参数 Map 构建(保留兼容)
|
|
|
|
|
+ signContent = buildSignContent(params);
|
|
|
|
|
+ }
|
|
|
String signStr = signContent + ticket;
|
|
String signStr = signContent + ticket;
|
|
|
String computedSign = md5(signStr);
|
|
String computedSign = md5(signStr);
|
|
|
|
|
|
|
@@ -661,12 +673,73 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
|
|
|
if (!isValid) {
|
|
if (!isValid) {
|
|
|
log.warn("签名验证失败 - 计算签名: {}, 接收签名: {}, 签名原文: {}",
|
|
log.warn("签名验证失败 - 计算签名: {}, 接收签名: {}, 签名原文: {}",
|
|
|
computedSign, receivedSign, signStr);
|
|
computedSign, receivedSign, signStr);
|
|
|
|
|
+ // 如果 rawBody 方式失败,尝试用旧方式再验一次(可能是 JSON 格式的回调)
|
|
|
|
|
+ if (rawBody != null && !rawBody.isEmpty()) {
|
|
|
|
|
+ String fallbackSignContent = buildSignContent(params);
|
|
|
|
|
+ String fallbackSignStr = fallbackSignContent + ticket;
|
|
|
|
|
+ String fallbackSign = md5(fallbackSignStr);
|
|
|
|
|
+ if (fallbackSign.equalsIgnoreCase(receivedSign)) {
|
|
|
|
|
+ log.info("签名验证通过(fallback 方式)");
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ log.warn("两种签名方式均验证失败 - rawBody方式: {}, fallback方式: {}",
|
|
|
|
|
+ computedSign, fallbackSign);
|
|
|
|
|
+ }
|
|
|
} else {
|
|
} else {
|
|
|
- log.debug("签名验证通过");
|
|
|
|
|
|
|
+ log.debug("签名验证通过(rawBody方式)");
|
|
|
}
|
|
}
|
|
|
return isValid;
|
|
return isValid;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从原始 application/x-www-form-urlencoded 请求体构建待签名字符串
|
|
|
|
|
+ * 保留 PHP 端的原始 URL 编码,避免 Spring decode → Java URLEncoder re-encode 的编码差异
|
|
|
|
|
+ *
|
|
|
|
|
+ * 原始 body 格式: k1=v1&k2=v2&...(值已 URL 编码)
|
|
|
|
|
+ * 处理方法:按键名排序后重新拼接,保持值的原始编码不变
|
|
|
|
|
+ */
|
|
|
|
|
+ private String buildSignContentFromRawBody(String rawBody) {
|
|
|
|
|
+ if (rawBody == null || rawBody.isEmpty()) {
|
|
|
|
|
+ return "";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 解析原始 body 为 key=value 对,保持值的原始编码
|
|
|
|
|
+ List<NameValuePair> pairs = new ArrayList<>();
|
|
|
|
|
+ String[] parts = rawBody.split("&");
|
|
|
|
|
+ for (String part : parts) {
|
|
|
|
|
+ int eqIdx = part.indexOf('=');
|
|
|
|
|
+ if (eqIdx > 0) {
|
|
|
|
|
+ String key = part.substring(0, eqIdx);
|
|
|
|
|
+ String value = part.substring(eqIdx + 1);
|
|
|
|
|
+ // 排除 sign 和 sign_type 参数
|
|
|
|
|
+ if (!"sign".equals(key) && !"sign_type".equals(key)) {
|
|
|
|
|
+ pairs.add(new NameValuePair(key, value));
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 按 key 字典序排序
|
|
|
|
|
+ pairs.sort(java.util.Comparator.comparing(p -> p.key));
|
|
|
|
|
+
|
|
|
|
|
+ // 拼接签名原文: k1=v1&k2=v2&...
|
|
|
|
|
+ return pairs.stream()
|
|
|
|
|
+ .map(p -> p.key + "=" + p.value)
|
|
|
|
|
+ .collect(Collectors.joining("&"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 简单的 key-value 对(内部使用)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static class NameValuePair {
|
|
|
|
|
+ final String key;
|
|
|
|
|
+ final String value;
|
|
|
|
|
+
|
|
|
|
|
+ NameValuePair(String key, String value) {
|
|
|
|
|
+ this.key = key;
|
|
|
|
|
+ this.value = value;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 构建待签名字符串
|
|
* 构建待签名字符串
|
|
|
* 将所有参数(排除 sign, sign_type)按 key 字典序排序,
|
|
* 将所有参数(排除 sign, sign_type)按 key 字典序排序,
|