Prechádzať zdrojové kódy

feat(unionPay):重构银联支付工具类并增强签名验证功能

- 删除旧的 `MD5Util`、`StringSortingUtil` 和 `UnionPayUtil` 工具类- 新增 `UnionPayUtils` 统一处理银联支付相关逻辑
- 增加 JSON 签名验证方法 `verifySignature`
- 支持参数提取、排序、签名生成与校验完整流程- 使用 SHA-256 算法替代原有 MD5 实现更安全签名机制
- 提供订单号生成方法支持多种业务类型区分
wangzq 1 deň pred
rodič
commit
f43a1ac4f9

+ 0 - 42
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/unionPay/MD5Util.java

@@ -1,42 +0,0 @@
-package org.jeecg.modules.pay.unionPay;
-
-import java.security.MessageDigest;
-
-public class MD5Util {
-    private static String byteArrayToHexString(byte b[]) {
-        StringBuffer resultSb = new StringBuffer();
-        for (int i = 0; i < b.length; i++)
-            resultSb.append(byteToHexString(b[i]));
-
-        return resultSb.toString();
-    }
-
-    private static String byteToHexString(byte b) {
-        int n = b;
-        if (n < 0)
-            n += 256;
-        int d1 = n / 16;
-        int d2 = n % 16;
-        return hexDigits[d1] + hexDigits[d2];
-    }
-
-    public static String MD5Encode(String origin, String charsetname) {
-        String resultString = null;
-        try {
-            resultString = new String(origin);
-            MessageDigest md = MessageDigest.getInstance("MD5");
-            if (charsetname == null || "".equals(charsetname))
-                resultString = byteArrayToHexString(md.digest(resultString
-                        .getBytes()));
-            else
-                resultString = byteArrayToHexString(md.digest(resultString
-                        .getBytes(charsetname)));
-        } catch (Exception exception) {
-        }
-        return resultString;
-    }
-
-    private static final String hexDigits[] = { "0", "1", "2", "3", "4", "5",
-            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
-
-}

+ 0 - 62
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/unionPay/StringSortingUtil.java

@@ -1,62 +0,0 @@
-package org.jeecg.modules.pay.unionPay;
-
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.text.SimpleDateFormat;
-import java.util.*;
-
-public class StringSortingUtil {
-
-    public static void main(String[] args) throws UnsupportedEncodingException {
-        // 示例用法
-        String signKey = "udik876ehjde32dU61edsxsf";
-        TreeMap<String, String> treeMap = new TreeMap<String, String>();
-        treeMap.put("version", "20191031");
-        treeMap.put("msgId", "4217");
-        treeMap.put("msgType", "wx.appPreOrder");
-        treeMap.put("requestTimestamp", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
-        treeMap.put("expireTime", "");
-        treeMap.put("merOrderId", "4217" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()));
-        treeMap.put("mid", "8981200539800CH");
-        treeMap.put("tid", "01672395");
-        treeMap.put("instMid", "APPDEFAULT");
-        treeMap.put("attachedData", "");
-        treeMap.put("orderDesc", "");
-        treeMap.put("originalAmount", "");
-        treeMap.put("totalAmount", "1");
-        treeMap.put("notifyUrl", "https://dhjt-uat.chinaums.com/test/notify");
-        treeMap.put("signType", "SHA256");
-
-        String sortedResult = sortAndFormatParams(treeMap);
-        System.out.println("按字典序排序后的参数和字符串:");
-        System.out.println(sortedResult);
-    }
-
-    public static String sortAndFormatParams(Map<String, String> params) throws UnsupportedEncodingException {
-        // 将Map转换为List以便排序
-        List<Map.Entry<String, String>> entries = new ArrayList<>(params.entrySet());
-
-        // 使用自定义的Comparator按照字符串的字典序(ASCII码顺序)排序
-        //entries.sort(Map.Entry.comparingByValue());
-        entries.sort(new Comparator<Map.Entry<String, String>>() {
-            @Override
-            public int compare(Map.Entry<String, String> entry1, Map.Entry<String, String> entry2) {
-                return entry1.getValue().compareTo(entry2.getValue());
-            }
-        });
-
-        // 构建格式化后的字符串
-        StringBuilder sortedString = new StringBuilder();
-        for (Map.Entry<String, String> entry : entries) {
-            sortedString.append(entry.getKey()).append("=").append( URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8)).append("&");
-        }
-
-        // 删除末尾多余的&号和空格
-        if (sortedString.length() > 0) {
-            sortedString.delete(sortedString.length() - 2, sortedString.length());
-        }
-
-        return sortedString.toString();
-    }
-}

+ 0 - 53
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/unionPay/UnionPayUtil.java

@@ -1,53 +0,0 @@
-package org.jeecg.modules.pay.unionPay;
-
-import cn.hutool.core.date.DateUtil;
-import cn.hutool.core.util.RandomUtil;
-import lombok.extern.slf4j.Slf4j;
-import org.jeecg.common.constant.CommonConstant;
-
-import java.util.*;
-
-@Slf4j
-public class UnionPayUtil {
-
-    public static final String UNION_PAY_URL = "https://dhjt-uat.chinaums.com/queryService/UmsWebPayPlugins";
-
-    /**
-     * @return String 订单号
-     * @Description 订单编号生成逻辑
-     * @params 类型:0-(D:订单) 1-(T:退单) 2-(B:保单)
-     **/
-    private String genOrderNumber(String msgId,Integer type) {
-        String format = DateUtil.format(new Date(), "yyyyMMddHHmmssSSS");
-        //随机数
-        String randomNumbers = RandomUtil.randomNumbers(CommonConstant.NUMBER_7);
-        return msgId + format + randomNumbers;
-    }
-
-    public static String createSign(String characterEncoding, SortedMap<Object,Object> parameters, String key){
-        StringBuffer sb = new StringBuffer();
-        StringBuffer sbkey = new StringBuffer();
-        //所有参与传参的参数按照ACCSII排序(升序)
-        Set<Map.Entry<Object, Object>> es = parameters.entrySet();
-        Iterator<Map.Entry<Object, Object>> it = es.iterator();
-        while(it.hasNext()) {
-            Map.Entry entry = (Map.Entry)it.next();
-            String k = (String)entry.getKey();
-            Object v = entry.getValue();
-            //空值不传递,不参与签名组串
-            if(null != v && !"".equals(v)) {
-                sb.append(k + "=" + v + "&");
-                sbkey.append(k).append("=").append(v).append("&");
-            }
-        }
-        //log.info("字符串:"+sb.toString());
-        sbkey.deleteCharAt(sb.length() - 1);
-        sbkey=sbkey.append(key);
-        log.info("字符串:"+sbkey.toString());
-        //MD5加密,结果转换为大写字符
-        String sign = MD5Util.MD5Encode(sbkey.toString(), characterEncoding).toUpperCase();
-        log.info("MD5加密值:"+sign);
-        return sb.toString()+"sign="+sign;
-    }
-
-}

+ 229 - 0
national-motion-module-system/national-motion-system-biz/src/main/java/org/jeecg/modules/pay/unionPay/UnionPayUtils.java

@@ -0,0 +1,229 @@
+package org.jeecg.modules.pay.unionPay;
+
+import cn.hutool.core.date.DateUtil;
+import cn.hutool.core.util.RandomUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang.StringUtils;
+import org.jeecg.common.constant.CommonConstant;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * @author wzq
+ */
+@Slf4j
+public class UnionPayUtils {
+
+    public static void main(String[] args) throws UnsupportedEncodingException {
+        String jsonString = "{\n" +
+                "    \"id\": \"\",\n" +
+                "    \"name\": \"test\",\n" +
+                "    \"age\": 25,\n" +
+                "    \"timestamp\": 1633046400,\n" +
+                "    \"sign\": \"0ddf80902bbd92cf2686f229d7d05f64da7f2a298e2433072701cfb39fa69a77\",\n" +
+                "    \"xxx\": \"\"\n" +
+                "}";
+        String secretKey = "udik876ehjde32dU61edsxsf";
+        boolean isValid = verifySignature(jsonString, secretKey);
+        log.info("签名验证结果: {}", isValid ? "通过" : "不通过");
+    }
+
+    /**
+     * 银联支付接口地址
+     */
+    public static final String UNION_PAY_URL = "https://dhjt-uat.chinaums.com/queryService/UmsWebPayPlugins";
+
+    /**
+     * 验证JSON字符串的签名
+     * @param jsonString 待验证的JSON字符串
+     * @param secretKey 签名密钥
+     * @return 验证结果
+     */
+    public static boolean verifySignature(String jsonString, String secretKey) {
+        try {
+            // 1. 解析JSON字符串获取所有参数(除sign外)
+            Map<String, String> paramsMap = extractParamsFromJson(jsonString);
+            log.info("参数列表: {}", paramsMap);
+
+            // 2. 获取sign参数值
+            String signValue = getSignValueFromJson(jsonString);
+            if (signValue == null || signValue.isEmpty()) {
+                throw new IllegalArgumentException("JSON字符串中缺少sign参数");
+            }
+            log.info("sign参数值: {}", signValue);
+
+            // 3. 排序参数并构建待签名字符串
+            String stringA = buildStringToSign(paramsMap, secretKey);
+            log.info("待签名字符串: {}", stringA);
+
+            // 4. 生成签名
+            String localSign = generateSignature(stringA);
+            log.info("本地签名: {}", localSign);
+
+            // 5. 比较签名(忽略大小写)
+            return localSign.equalsIgnoreCase(signValue);
+
+        } catch (Exception e) {
+            throw new RuntimeException("签名验证失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * @return 订单号
+     * @Author wzq
+     * @Description 订单编号生成逻辑
+     * @params 类型:0-(D:订单) 1-(T:退单) 2-(B:保单)
+     **/
+    private String genOrderNum(String msgId, int type) {
+        String format = DateUtil.format(new Date(), "yyyyMMddHHmmssSSS");
+        String nextInt = RandomUtil.randomNumbers(CommonConstant.NUMBER_7);
+        if (type == 0) {
+            return msgId + "D" + format + nextInt;
+        } else {
+            return msgId + "T" + format + nextInt;
+        }
+    }
+
+    /**
+     * 从JSON中获取sign参数的值
+     */
+    private static String getSignValueFromJson(String jsonString)
+            throws JsonProcessingException {
+        ObjectMapper objectMapper = new ObjectMapper();
+        JsonNode rootNode = objectMapper.readTree(jsonString);
+        JsonNode signNode = rootNode.get("sign");
+        return (signNode != null && !signNode.isNull()) ? signNode.asText() : null;
+    }
+
+    /**
+     * 从JSON字符串中提取所有参数(除了sign)
+     */
+    public static Map<String, String> extractParamsFromJson(String jsonString) throws Exception {
+        ObjectMapper objectMapper = new ObjectMapper();
+        JsonNode rootNode = objectMapper.readTree(jsonString);
+
+        Map<String, String> paramsMap = new HashMap<>();
+        traverseJsonNode(rootNode, "", paramsMap);
+        paramsMap.remove("sign");
+        return paramsMap;
+    }
+
+    private static void traverseJsonNode(JsonNode node, String currentPath, Map<String, String> paramsMap) {
+        if (node.isObject()) {
+            node.fields().forEachRemaining(entry -> {
+                String newPath = currentPath.isEmpty() ? entry.getKey() : currentPath + "." + entry.getKey();
+                traverseJsonNode(entry.getValue(), newPath, paramsMap);
+            });
+        } else if (node.isArray()) {
+            for (int i = 0; i < node.size(); i++) {
+                traverseJsonNode(node.get(i), currentPath + "[" + i + "]", paramsMap);
+            }
+        } else if (node.isValueNode()) {
+            // 只处理值节点
+            if (!node.isNull()) {
+                // 排除JsonNode类型的NullNode
+                String value = node.asText();
+                // 对于非Null节点,获取其文本表示
+                if (value != null && !value.isEmpty()) {
+                    // 进一步排除空字符串
+                    paramsMap.put(currentPath, value);
+                }
+            }
+        }
+    }
+
+    /**
+     * 构建待签名字符串
+     */
+    private static String buildStringToSign(Map<String, String> paramsMap, String secretKey) {
+        String params = "";
+        try {
+            // 按key字典序排序
+            List<String> sortedKeys = new ArrayList<>(paramsMap.keySet());
+            Collections.sort(sortedKeys);
+
+            StringBuilder sb = new StringBuilder();
+
+            // 拼接参数键值对
+            for (String key : sortedKeys) {
+                String value = URLEncoder.encode(paramsMap.get(key), StandardCharsets.UTF_8);
+                // 跳过空值参数
+                if (value != null && !value.trim().isEmpty()) {
+                    if (sb.length() > 0) {
+                        sb.append("&");
+                    }
+                    sb.append(key).append("=").append(value);
+                }
+            }
+            // 删除末尾多余的&号和空格
+            params = sb.toString();
+            if (!params.isEmpty()) {
+                params = params.substring(0, params.length() - 1);
+            }
+            params+= params+ secretKey;
+        } catch (Exception e) {
+            return "";
+        }
+        return params;
+    }
+
+    /**
+     * 生成签名(使用SHA-256算法)
+     */
+    private static String generateSignature(String stringToSign) {
+        return SHA256Util.encrypt(stringToSign);
+    }
+
+    /**
+     *
+     * 遍历集合M,取出全部的参数key,将参数key字典序排列,根据排序后的参数key获取对应的value,将非空的参数key和参数value用"="拼接,多个参数之间用"&"拼接
+     *
+     * @param param   参数
+     * @param encode  编码
+     * @param isLower 是否小写
+     * @return
+     */
+    public static String formatUrlParam(Map<String, String> param, String encode, boolean isLower) {
+        String params = "";
+
+        try {
+            List<Map.Entry<String, String>> items = new ArrayList<>(param.entrySet());
+
+            //对所有传入的参数按照字段名从小到大排序
+            items.sort(Map.Entry.comparingByKey());
+
+            //构造URL 键值对的形式
+            StringBuffer sb = new StringBuffer();
+            for (Map.Entry<String, String> item : items) {
+                if (StringUtils.isNotBlank(item.getKey())) {
+                    String key = item.getKey();
+                    String val = item.getValue();
+                    val = URLEncoder.encode(val, encode);
+                    if (isLower) {
+                        sb.append(key.toLowerCase()).append("=").append(val);
+                    } else {
+                        sb.append(key).append("=").append(val);
+                    }
+                    sb.append("&");
+                }
+            }
+            // 删除末尾多余的&号和空格
+            params = sb.toString();
+            if (!params.isEmpty()) {
+                params = params.substring(0, params.length() - 1);
+            }
+        } catch (Exception e) {
+            return "";
+        }
+        return params;
+    }
+
+}