Bladeren bron

fix: 按官方文档修正签名算法:值URL编码 + ticket直接拼接

官方文档明确:
1. 参数值需URL编码后再拼接(JSON中的{}"等字符编码为%7B%22格式)
2. ticket直接拼接到参数字符串末尾,无 & 或 = 分隔符
之前错误地使用了 &ticket=<value> 格式且未URL编码参数值。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 weken geleden
bovenliggende
commit
ee9695769f
1 gewijzigde bestanden met toevoegingen van 38 en 39 verwijderingen
  1. 38 39
      haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

+ 38 - 39
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -40,6 +40,8 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.concurrent.TimeUnit;
@@ -538,9 +540,6 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
         }
     }
 
-    @Value("${haha.api.app-secret}")
-    private String appSecret;
-
     @Override
     public boolean validateSign(Map<String, Object> params) {
         String receivedSign = extractParam(params, "sign");
@@ -549,61 +548,61 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             return false;
         }
 
-        String signContent = buildSignContent(params);
-
-        // 诊断模式:同时尝试 appsecret 和 ticket 两种方式,日志中明确标注匹配情况
-        // 待确认哪种方式正确后,去掉不需要的分支
-        boolean matched = false;
-
-        // 方式1:末尾拼接 &appsecret=<appSecret>(哈哈API文档描述的方式)
-        if (appSecret != null && !appSecret.isEmpty()) {
-            String sign1 = md5(signContent + "&appsecret=" + appSecret);
-            if (sign1.equalsIgnoreCase(receivedSign)) {
-                log.debug("签名验证通过 (appsecret方式)");
-                matched = true;
-            }
+        // 获取 ticket(从 /token/get 接口返回)
+        String ticket;
+        try {
+            hahaClient.getAccessToken();
+            ticket = hahaClient.getTicket();
+        } catch (Exception e) {
+            log.error("获取验签 ticket 失败", e);
+            return false;
         }
 
-        // 方式2:末尾拼接 &ticket=<ticket>(SDK CallbackUtils 描述的方式)
-        if (!matched) {
-            String ticket = null;
-            try {
-                hahaClient.getAccessToken();
-                ticket = hahaClient.getTicket();
-            } catch (Exception e) {
-                log.debug("获取 ticket 失败: {}", e.getMessage());
-            }
-            if (ticket != null && !ticket.isEmpty()) {
-                String sign2 = md5(signContent + "&ticket=" + ticket);
-                if (sign2.equalsIgnoreCase(receivedSign)) {
-                    log.debug("签名验证通过 (ticket方式)");
-                    matched = true;
-                }
-            }
+        if (ticket == null || ticket.isEmpty()) {
+            log.error("ticket 为空,无法验证签名");
+            return false;
         }
 
-        if (!matched) {
-            log.warn("签名验证失败(两种方式均不匹配) - 接收签名: {}, 签名内容: {}",
-                    receivedSign, signContent);
-        }
+        // 哈哈平台回调签名规则(官方文档):
+        // 第一步:参数按键排序,值 URL 编码后拼接为 k1=v1&k2=v2&...
+        // 第二步:末尾直接拼接 ticket(无 & 或 = 分隔符)
+        // 第三步:MD5 得到签名
+        String signContent = buildSignContent(params);
+        String signStr = signContent + ticket;
+        String computedSign = md5(signStr);
 
-        return matched;
+        boolean isValid = computedSign.equalsIgnoreCase(receivedSign);
+        if (!isValid) {
+            log.warn("签名验证失败 - 计算签名: {}, 接收签名: {}, 签名原文: {}",
+                    computedSign, receivedSign, signStr);
+        } else {
+            log.debug("签名验证通过");
+        }
+        return isValid;
     }
 
     /**
      * 构建待签名字符串
      * 将所有参数(排除 sign, sign_type)按 key 字典序排序,
-     * 拼接为 key=value 格式,以 & 连接
+     * 值进行 URL 编码后拼接为 key=value 格式,以 & 连接
      */
     private String buildSignContent(Map<String, Object> params) {
         return params.entrySet().stream()
                 .filter(e -> !"sign".equals(e.getKey()) && !"sign_type".equals(e.getKey()))
                 .filter(e -> e.getValue() != null)
                 .sorted(Map.Entry.comparingByKey())
-                .map(e -> e.getKey() + "=" + e.getValue().toString())
+                .map(e -> e.getKey() + "=" + urlEncode(e.getValue().toString()))
                 .collect(Collectors.joining("&"));
     }
 
+    private String urlEncode(String value) {
+        try {
+            return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
+        } catch (Exception e) {
+            return value;
+        }
+    }
+
     /**
      * MD5 加密(32位小写十六进制)
      */