Эх сурвалжийг харах

fix(omg-pay): CheckMacValue 改用大小写不敏感排序,修复 QueryTradeInfo 验签(bugB)

生产 4 候选诊断坐实根因:OMG 服务端 CheckMacValue 按字母序大小写不敏感排序
计算(receivedCMV==ci_inc,AC1C1AFF2195...),原实现用 TreeMap naturalOrder
(ASCII 大小写敏感),对 QueryTradeInfo 响应的混合大小写字段(AlipayID/
MerchantID/TradeStatus 等首字母大写 vs amount/auth_code/card4no/eci/gwsr/
process_date/red_dan 等首字母小写)排序错位 → CMV 算不对 → verify 失败 →
queryTrade 补单读不到 TradeStatus(订单 991786433092835 id=26 已付判未付即此)。

- generate 默认比较器改为 String.CASE_INSENSITIVE_ORDER(空值保留不变)。
- 该改动同时修复 queryTrade/notify/paymentInfo 三处共用 verify;createAioForm
  出站请求参数全首字母大写,两种排序结果相同,无回归(main 物流向量 MD5 PASS)。
- 补回归测试 verifiesQueryTradeInfoResponseWithCaseInsensitiveKeyOrder:构造
  混合大小写字段响应,先断言两种排序产生不同签名(保证用例有效性),再断言
  verify() 通过(用 ECPay 公开测试凭证,不依赖真实门店凭证)。
- 算法 Javadoc 更新:排序大小写不敏感 + bugB 由生产 4 候选诊断坐实。

Co-Authored-By: Claude <noreply@anthropic.com>
qmj 2 долоо хоног өмнө
parent
commit
1a424c3afe

+ 11 - 3
ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgCheckMacValue.java

@@ -15,9 +15,12 @@ import java.util.TreeMap;
  * 请求与回调都是「明文参数 + 单个 CheckMacValue」。本类独立于 {@code newebpay} 包,绝不复用
  * {@code NewebPayEncryptUtil}。
  *
- * <p>算法(contracts/api.md §C4 / research D1;已用 ECPay 物流官方向量 MD5 ground truth 验证编码 pipeline):
+ * <p>算法(contracts/api.md §C4 / research D1;编码 pipeline 已用 ECPay 物流官方向量 MD5 ground truth 验证;
+ * <b>但该向量字段全首字母大写,无法暴露排序大小写分歧——排序大小写由生产 QueryTradeInfo 响应实测另行锁定</b>):
  * <ol>
- *   <li>去掉 {@code CheckMacValue} 本身;其余所有参数(包括空值)按 key 字母序升序。</li>
+ *   <li>去掉 {@code CheckMacValue} 本身;其余所有参数(包括空值)按 key <b>字母序、大小写不敏感</b>
+ *       ({@link String#CASE_INSENSITIVE_ORDER})升序。OMG 服务端同此;QueryTradeInfo 响应字段大小写
+ *       混合,大小写敏感排序会分歧致验签失败(bugB,2026-08-12 生产 4 候选诊断 receivedCMV==ci_inc 坐实)。</li>
  *   <li>拼 {@code k1=v1&k2=v2...}。</li>
  *   <li>包夹:{@code HashKey={key}&{query}&HashIV={iv}}。</li>
  *   <li>.NET 风格 URL 编码:{@code URLEncoder.encode(整串)}({@code & = / :} 与空格→{@code +} 一并被编码,
@@ -49,7 +52,12 @@ public final class OmgCheckMacValue {
 
     /** 指定摘要算法生成(SHA-256 用于 OMG 生产;MD5 仅供自测对照 ECPay 旧版向量)。 */
     static String generate(Map<String, String> params, String hashKey, String hashIv, String algorithm) {
-        return generate(params, hashKey, hashIv, Comparator.naturalOrder(), false, algorithm);
+        // 大小写不敏感排序:String.CASE_INSENSITIVE_ORDER。OMG(ECPay 同源引擎)服务端按字母序不分大小写
+        // 排序算 CheckMacValue。QueryTradeInfo 响应字段大小写混合(AlipayID/MerchantID/TradeStatus 等
+        // 首字母大写 vs amount/auth_code/card4no/eci/gwsr/process_date/red_dan 等首字母小写),大小写敏感
+        // 排序会与 OMG 分歧致验签失败(bugB,2026-08-12 由生产 4 候选诊断 receivedCMV==ci_inc 坐实)。
+        // 出站请求参数全为首字母大写,两种排序结果相同,故对本签名无回归。
+        return generate(params, hashKey, hashIv, String.CASE_INSENSITIVE_ORDER, false, algorithm);
     }
 
     /**

+ 51 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/utils/omg/OmgCheckMacValueTest.java

@@ -2,11 +2,13 @@ package com.ruoyi.app.utils.omg;
 
 import org.junit.jupiter.api.Test;
 
+import java.util.Comparator;
 import java.util.LinkedHashMap;
 import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class OmgCheckMacValueTest {
@@ -51,4 +53,53 @@ class OmgCheckMacValueTest {
         params.put("CheckMacValue", "0".repeat(64));
         assertFalse(OmgCheckMacValue.verify(params, "key", "iv"));
     }
+
+    /**
+     * 回归 bugB(2026-08-12):OMG QueryTradeInfo 响应字段大小写混合,服务端 CheckMacValue 按
+     * 大小写不敏感排序计算。旧实现用 Comparator.naturalOrder()(ASCII 大小写敏感)排序,混合字段下
+     * 与 OMG 分歧 → 验签失败(生产订单 991786433092835 实测 receivedCMV==ci_inc 坐实)。
+     *
+     * <p>本用例构造同形响应(首字母大写的 AlipayID/CustomField1/MerchantID 与首字母小写的
+     * amount/auth_code/card4no/eci/gwsr/process_date/red_dan 并存),先用「大小写不敏感」生成 OMG 侧
+     * 签名,再断言 verify() 通过。修复前 verify 走 naturalOrder 会算出不同签名而失败。
+     */
+    @Test
+    void verifiesQueryTradeInfoResponseWithCaseInsensitiveKeyOrder() {
+        Map<String, String> resp = new LinkedHashMap<>();
+        resp.put("MerchantID", "1000031");
+        resp.put("MerchantTradeNo", "OMG10C9AA21B1814EE29");
+        resp.put("TradeAmt", "97");
+        resp.put("TradeDate", "2026/08/11 17:06:42");
+        resp.put("TradeNo", "2608111706428239");
+        resp.put("TradeStatus", "10200095");
+        resp.put("ItemName", "order 991786433092835");
+        resp.put("HandlingCharge", "0");
+        resp.put("PaymentTypeChargeFee", "0");
+        // §2 额外回传(空值),首字母大小写混合 —— 触发排序分歧的关键
+        resp.put("AlipayID", "");
+        resp.put("CustomField1", "");
+        resp.put("amount", "");
+        resp.put("auth_code", "");
+        resp.put("card4no", "");
+        resp.put("eci", "");
+        resp.put("gwsr", "");
+        resp.put("process_date", "");
+        resp.put("red_dan", "");
+
+        String key = "5294y06JbISpM5x9";
+        String iv = "v77hoKGq4kWxNNIS";
+
+        // OMG 侧签名:大小写不敏感排序,保留空值字段
+        String omgCmv = OmgCheckMacValue.generate(resp, key, iv,
+                String.CASE_INSENSITIVE_ORDER, false);
+        // 前置断言:本字段集必须使两种排序产生不同签名,否则用例无法锁定该 bug
+        String sensitiveCmv = OmgCheckMacValue.generate(resp, key, iv,
+                Comparator.naturalOrder(), false);
+        assertNotEquals(sensitiveCmv, omgCmv,
+                "测试字段集必须使大小写敏感/不敏感排序产生不同签名,否则用例无效");
+
+        resp.put("CheckMacValue", omgCmv);
+        assertTrue(OmgCheckMacValue.verify(resp, key, iv),
+                "verify 必须按大小写不敏感排序以匹配 OMG 服务端算法");
+    }
 }