|
| 1 | +package com.github.binarywang.wxpay.bean.notify; |
| 2 | + |
| 3 | +import com.github.binarywang.wxpay.constant.WxPayConstants; |
| 4 | +import org.apache.commons.codec.digest.DigestUtils; |
| 5 | +import org.testng.Assert; |
| 6 | +import org.testng.annotations.Test; |
| 7 | + |
| 8 | +import java.util.*; |
| 9 | + |
| 10 | +/** |
| 11 | + * 测试当微信支付回调 XML 包含未在 Java Bean 中定义的字段时,签名验证是否正常。 |
| 12 | + * <p> |
| 13 | + * 问题背景:当微信返回的 XML 包含某些未在 WxPayOrderNotifyResult 中定义的字段时, |
| 14 | + * 这些字段会被微信服务器用于签名计算。如果 toMap() 方法丢失了这些字段, |
| 15 | + * 则签名验证会失败,抛出 "参数格式校验错误!" 异常。 |
| 16 | + * </p> |
| 17 | + * <p> |
| 18 | + * 解决方案:修改 WxPayOrderNotifyResult.toMap() 方法,使用父类的 toMap() 方法 |
| 19 | + * 直接从原始 XML 解析所有字段,而不是使用 SignUtils.xmlBean2Map(this)。 |
| 20 | + * </p> |
| 21 | + * |
| 22 | + * @see <a href="https://github.com/binarywang/WxJava/issues/3750">Issue #3750</a> |
| 23 | + */ |
| 24 | +public class WxPayOrderNotifyUnknownFieldTest { |
| 25 | + |
| 26 | + private static final String MCH_KEY = "testmchkey1234567890123456789012"; |
| 27 | + private static final List<String> NO_SIGN_PARAMS = Arrays.asList("sign", "key", "xmlString", "xmlDoc", "couponList"); |
| 28 | + |
| 29 | + @Test |
| 30 | + public void testSignatureWithUnknownField() throws Exception { |
| 31 | + // 创建一个测试用的 XML,包含一个未知字段 (未在 WxPayOrderNotifyResult 中定义) |
| 32 | + Map<String, String> params = new LinkedHashMap<>(); |
| 33 | + params.put("appid", "wx58ff40508696691f"); |
| 34 | + params.put("bank_type", "ICBC_DEBIT"); |
| 35 | + params.put("cash_fee", "1"); |
| 36 | + params.put("fee_type", "CNY"); |
| 37 | + params.put("is_subscribe", "N"); |
| 38 | + params.put("mch_id", "1545462911"); |
| 39 | + params.put("nonce_str", "1761723102373"); |
| 40 | + params.put("openid", "o1gdd16CZCi6yYvkn6j9EB_1TObM"); |
| 41 | + params.put("out_trade_no", "20251029153140"); |
| 42 | + params.put("result_code", "SUCCESS"); |
| 43 | + params.put("return_code", "SUCCESS"); |
| 44 | + params.put("time_end", "20251029153852"); |
| 45 | + params.put("total_fee", "1"); |
| 46 | + params.put("trade_type", "JSAPI"); |
| 47 | + params.put("transaction_id", "4200002882220251029816273963B"); |
| 48 | + // 添加一个未知字段 |
| 49 | + params.put("unknown_field", "unknown_value"); |
| 50 | + |
| 51 | + // 计算正确的签名 (包含未知字段) |
| 52 | + String correctSign = createSign(params, WxPayConstants.SignType.MD5, MCH_KEY); |
| 53 | + params.put("sign", correctSign); |
| 54 | + |
| 55 | + // 创建 XML |
| 56 | + StringBuilder xmlBuilder = new StringBuilder("<xml>"); |
| 57 | + for (Map.Entry<String, String> entry : params.entrySet()) { |
| 58 | + xmlBuilder.append("<").append(entry.getKey()).append(">") |
| 59 | + .append(entry.getValue()) |
| 60 | + .append("</").append(entry.getKey()).append(">"); |
| 61 | + } |
| 62 | + xmlBuilder.append("</xml>"); |
| 63 | + String xml = xmlBuilder.toString(); |
| 64 | + |
| 65 | + System.out.println("测试 XML (包含未知字段 unknown_field):"); |
| 66 | + System.out.println(xml); |
| 67 | + System.out.println("正确的签名 (包含未知字段计算): " + correctSign); |
| 68 | + |
| 69 | + // 解析 XML |
| 70 | + WxPayOrderNotifyResult result = WxPayOrderNotifyResult.fromXML(xml); |
| 71 | + Map<String, String> beanMap = result.toMap(); |
| 72 | + |
| 73 | + System.out.println("\ntoMap() 结果:"); |
| 74 | + TreeMap<String, String> sortedMap = new TreeMap<>(beanMap); |
| 75 | + for (Map.Entry<String, String> entry : sortedMap.entrySet()) { |
| 76 | + System.out.println(" " + entry.getKey() + " = " + entry.getValue()); |
| 77 | + } |
| 78 | + |
| 79 | + // 检查 unknown_field 是否存在 |
| 80 | + boolean hasUnknownField = beanMap.containsKey("unknown_field"); |
| 81 | + System.out.println("\ntoMap() 是否包含 unknown_field: " + hasUnknownField); |
| 82 | + |
| 83 | + // 验证签名 |
| 84 | + String verifySign = createSign(beanMap, WxPayConstants.SignType.MD5, MCH_KEY); |
| 85 | + System.out.println("原始签名: " + result.getSign()); |
| 86 | + System.out.println("计算签名: " + verifySign); |
| 87 | + |
| 88 | + // 这个测试验证修复后 toMap() 能正确包含所有字段 |
| 89 | + Assert.assertTrue(hasUnknownField, "toMap() 应该包含 unknown_field"); |
| 90 | + Assert.assertEquals(verifySign, result.getSign(), "签名应该匹配"); |
| 91 | + } |
| 92 | + |
| 93 | + private static String createSign(Map<String, String> params, String signType, String signKey) { |
| 94 | + StringBuilder toSign = new StringBuilder(); |
| 95 | + for (String key : new TreeMap<>(params).keySet()) { |
| 96 | + String value = params.get(key); |
| 97 | + if (value != null && !value.isEmpty() && !NO_SIGN_PARAMS.contains(key)) { |
| 98 | + toSign.append(key).append("=").append(value).append("&"); |
| 99 | + } |
| 100 | + } |
| 101 | + toSign.append("key=").append(signKey); |
| 102 | + return DigestUtils.md5Hex(toSign.toString()).toUpperCase(); |
| 103 | + } |
| 104 | +} |
0 commit comments