商户接入指南 商户接入指南
  • V2 (opens new window)
  • V3 (opens new window)
  • V4-English (opens new window)
  • V4-专业版 (opens new window)
  • V2 (opens new window)
  • V3 (opens new window)
  • V4-English (opens new window)
  • V4-专业版 (opens new window)
  • 概述

    • 起步
    • 对接流程
    • 联调资源
  • 服务

    • Checkout

      • 概览
      • Hosted
      • Non-Hosted
      • APIs

    • 订阅

      • 概览
      • 接入流程
    • 线下付款码支付

      • 接入流程
    • Tokenization

      • 概览
      • 接入流程
      • APIs

    • 风险管理

      • 概览
      • Dynamic 3D Secure
      • Disputes
      • APIs

    • 一键支付

      • 概览
      • 接入流程
      • APIs

    • ApplePay

    • GooglePay

      • 概览
      • 异步通知
      • 签名规约
        • 签名类型
        • 请求签名
        • 应答签名
        • 异步通知签名
        • 签名串组装
        • 计算签名串
        • 签名工具类
      • APIs

    • KYB

    • 支付链

    • 建站工具

    • 工具

  • 附录

  • v4

签名规约

# 签名规约

PingPongCheckout API v3通过验证签名来保证请求的真实性和数据的完整性。

# 签名类型

签名类型 描述
MD5 表示选择 MD5 算法,商户使用 Salt 对报文进行摘要签名和验签
SHA256 表示选择 SHA256算法,商户使用 Salt 对报文进行摘要签名和验签

# 请求签名

商户需要使用自身的私钥对消息体中关键数据的组合进行签名。没有携带签名或者签名验证不通过的请求,都不会被执行,并返回错误。


注意 :

请求参数都应该trim,加签应该在trim之后

请求签名是部分加签后,加签规则参考以下的请求签名范围


以下为PingPongCheckout API v3与调用方约定的请求加签参数列表

参数名 描述
accId PingPong商户店铺号
amount 交易金额
clientId PingPong商户号,当请求参数中没有传入clientId,不加入签名
cardNum 卡号
currency 交易币种
merchantTransactionId 商户交易流水号
requestId 请求ID
signType 加签类型
transactionId PingPong交易流水号

# 请求签名范围

注意 :

1. 加签以每个接口的请求参数为基础。

2. 在加签参数列表的请求参数参与加签

3. 请求参数中没有,加签参数列表有,不参与加签

4. 请求参数中有,加签参数列表没有,不参与加签

5. 综上:

设待加签参数集合C

设请求参数集合A

设加签参数集合B

则 C=A∩B 取两者交集

# 应答签名

对于签名验证成功的请求,PingPong支付API v3会对应答进行签名。为了保证安全性,应对应答进行验签。


注意 :

sign 字段不参与签名


# 异步通知签名

签名范围和签名方法同请求签名

# 签名串组装

字典序:按首字母进行排序;

queryString:用'=' 进行参数名和参数值(trim 后的值)的拼接,用'&'进行多 个参数之间的拼接,即 key1=val1&key2=val2&key3=val3

  • 对参数名按字典序排序后,按照queryString方式组装。

  • 签名秘钥(salt)放入签名串的位置为: 签名串的开头 , 即{salt}key1=val2&key2=val2&key3=val3

# 计算签名串

推荐使用SHA256签名方式,安全度高于MD5

# 签名工具类

    import com.google.common.collect.Maps;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.codec.digest.DigestUtils;
    import org.apache.commons.lang3.StringUtils;
    
    import java.security.MessageDigest;
    import java.util.Map;
    import java.util.TreeMap;
    
    @Slf4j
    public class Sign {
    
        /**
         * 部分参数签名,参与签名的字段
         */
        private static final String[] includeFields = {"accId", "amount", "clientId", "cardNum", "currency", "merchantTransactionId",
                "requestId", "signType", "transactionId"};
    
        /**
         * 签名秘钥
         */
        private String salt = null;
    
    
        public Sign(String salt) {
            this.salt = salt;
        }
    
        /**
         * 执行签名
         *
         * @param signType 签名类型
         * @param signMap  待签名串
         */
        public String signature(String signType, TreeMap<String, Object> signMap) {
    
            String  signContent = getPartSignParams(signMap);
            log.debug("signContent:{}", signContent);
    
            if (StringUtils.equalsIgnoreCase("MD5",signType)) {
                return md5Sign(salt, signContent);
            } else if (StringUtils.equalsIgnoreCase("SHA256",signType)) {
                return sha256(signContent, salt);
            }
            return null;
        }
    
    
        /**
         * 获取待签名串(部分字段签名)
         */
        private static String getPartSignParams(TreeMap<String, Object> signMap) {
            //添加需要签名的字段
            TreeMap<String, Object> resultMap = Maps.newTreeMap();
            for (String param : includeFields) {
                String value = (String) signMap.get(param);
                if (StringUtils.isNotBlank(value)) {
                    resultMap.put(param, value);
                }
            }
            return getSignParams(resultMap);
        }
    
        /**
         * 获取待签名串
         */
        private static String getSignParams(TreeMap<String, Object> resultMap) {
            StringBuilder stringBuilder = new StringBuilder();
            int paramNum = 0;
            for (Map.Entry<String, Object> signEntry : resultMap.entrySet()) {
                paramNum++;
                stringBuilder.append(signEntry.getKey());
                stringBuilder.append("=");
                stringBuilder.append(signEntry.getValue());
                if (paramNum < resultMap.size()) {
                    stringBuilder.append("&");
                }
            }
            log.debug("content:【{}】", stringBuilder);
            return stringBuilder.toString();
        }
    
    
        private static String md5Sign(String salt, String content) {
            try {
                MessageDigest md = MessageDigest.getInstance("MD5");
                md.update(salt.getBytes());
                md.update(content.getBytes());
                byte[] digest = md.digest();
                return byteToHexString(digest);
            } catch (Exception e) {
                log.error("md5签名失败", e);
            }
            return null;
        }
    
    
        private static String sha256(String content, String salt) {
            try {
                if (StringUtils.isBlank(salt)) {
                    throw new RuntimeException("salt is null");
                }
                String contentStr = salt.concat(content);
                return DigestUtils.sha256Hex(contentStr.getBytes("UTF-8")).toUpperCase();
            } catch (Exception e) {
                log.error("sha256", e);
            }
    
            return null;
        }
    
    
        public static String byteToHexString(byte[] b) {
            StringBuilder hexString = new StringBuilder();
            for (int i = 0; i < b.length; i++) {
                String hex = Integer.toHexString(b[i] & 0xFF);
                if (hex.length() == 1) {
                    hex = '0' + hex;
                }
                hexString.append(hex.toUpperCase());
            }
            return hexString.toString();
        }
    }
        
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    // Make sure to add code blocks to your code group
    上次更新: 2023/11/02, 18:33:10

    ← 异步通知 支付交易接口→

    杭州乒乓智能技术有限公司 | Copyright © 2015-2024 checkout.pingpongx.com.All Rights Reserved.
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式