跳至主要內容

签名规则

TOPPAY Team大约 9 分钟

概述

数字签名用于保证 API 请求(收款、付款、查询等)的完整性与不可否认性。对接时请严格按本文规则生成 sign,否则接口会返回签名校验失败。

对接前准备

步骤说明
1. 生成密钥对使用 RSA PKCS#8(推荐 2048 位),妥善保管商户私钥
2. 配置公钥TOPPAY 商户后台上传商户公钥(仅公钥上传至平台)
3. 区分两种签名请求签名:您用私钥生成;回调验签:使用平台公钥验证回调中的 sign
4. 回调公钥平台公钥在 后台 → API 配置 中查看,用于验证异步通知

签名规则(技术摘要)

项目说明
算法RSA(发起方使用私钥加密生成签名,接收方使用公钥解密比对;请严格按本文示例代码实现,勿与标准 RSA-SHA256 Sign/Verify 混淆)
编码签名字符串输出为 Base64
私钥格式PKCS#8(推荐 2048 位;历史商户可能使用 1024 位)

核心原则(务必遵守)

  1. 仅参数值参与拼接:参数名(Key)只用于排序,不参与签名字符串。
  2. 对所有非空参数的 Key 按 ASCII 升序排序。
  3. 按排序后的 Key 顺序依次取出参数值,直接首尾拼接得到待签名字符串,再使用 RSA 私钥加密得到 sign

示例:从参数到签名

示例参数说明

下列参数与「收款」类接口字段一致,便于您对照文档;是否必填以具体接口为准

参数类型必填说明示例值
mchNoString(32)商户号,商户平台 - 个人中心PHOT000001
methodString(16)支付方式GCASH
orderNumString(64)商户订单号T1642592278863
amountint(10)金额(与接口约定单位一致)10000
productDetailString(100)商品/订单描述Test Pay
downNotifyUrlString(164)异步通知地址your notify url
timestampString(32)请求时间戳(13 位毫秒级 Unix 时间戳)1749451858772
customerNameString(64)客户姓名JackMa
expiryPeriodint(5)订单有效时间(分钟)1440
customerEmailString(64)客户邮箱[email protected]

原始 JSON 示例(生成签名时不要包含 sign 字段):

{
  "mchNo": "PHOT000001",
  "method": "GCASH",
  "orderNum": "T1642592278863",
  "amount": 10000,
  "productDetail": "Test Pay",
  "downNotifyUrl": "your notify url",
  "timestamp": "1749451858772",
  "customerName": "JackMa",
  "expiryPeriod": 1440,
  "customerEmail": "[email protected]"
}

参数值与类型

  • 拼接时使用的是最终参与传输的值的字符串形式(与 JSON 序列化后一致)。例如数字 100001440 在拼接中为 "10000""1440" 对应的字符序列。
  • 生成签名前请排除 sign 字段;空字符串、null、缺省字段不参与签名。

步骤 1:参数 Key 按 ASCII 升序排序

将所有非空参数的 Key 按 ASCII 码升序排列:

排序参数名参数值
1amount10000
2customerEmail[email protected]
3customerNameJackMa
4downNotifyUrlyour notify url
5expiryPeriod1440
6mchNoPHOT000001
7methodGCASH
8orderNumT1642592278863
9productDetailTest Pay
10timestamp1749451858772

步骤 2:拼接待签名字符串 StrA

按上表顺序只取参数值无分隔符直接拼接:

StrA = [email protected] notify url1440PHOT000001GCASHT1642592278863Test Pay1749451858772

注意事项

  • 只拼接参数值,不要拼接参数名。
  • 空值不参与签名(不要出现在排序与拼接中)。
  • 参数值之间无任何分隔符(无 &=、换行等)。

步骤 3:计算签名

使用您在 TOPPAY 商户后台配置的 商户私钥(privateKey)StrA 做 RSA 加密,再 Base64 编码:

sign = RSA(StrA, privateKey)

将得到的 sign 放入请求体(字段名以接口文档为准,一般为 sign)。

签名结果示例

IMLn23c4orM+7pZhHoRmbjrol4X33jeAqFxbZuQ+pnznBIGhb6Ail3qQPmKwcuhNCt536nmldpbWI72
k1lDxd0zZ95ZHElcNzwTFHFKtd8063uy6rFaxaW6DQ47t4U/95dpGfHAZe0GiIFAQ6xQquaoLINyQa4QqL+cpB
JFEg1dyW6GYLFSdJnx7ycQvFYllmOpGZmdPLny62GvrCWvkiIARUsmc9fpkpTx5UQEDTgmhwdCKBkhHVsx2AiQ
bYDxZ5WBuU1GZeiJjPuzSxvzWP6VoQBsfpwTI5kdJs6aQCekGO2/YScD+tGgrm2J89Pc/axPcb1xZzsi5SxpWh
feabQ==

请求 JSON 示例

{
  "mchNo": "PHOT000001",
  "method": "GCASH",
  "orderNum": "T1642592278863",
  "amount": 10000,
  "productDetail": "Test Pay",
  "downNotifyUrl": "your notify url",
  "timestamp": "1749451858772",
  "customerName": "JackMa",
  "expiryPeriod": 1440,
  "customerEmail": "[email protected]",
  "sign": "IMLn23c4orM+7pZhHoRmbjrol4X33jeAqFxbZuQ+pnznBIGhb6Ail3qQPmKwcuhNCt536nmldpbWI72k1lDxd0zZ95ZHElcNzwTFHFKtd8063uy6rFaxaW6DQ47t4U/95dpGfHAZe0GiIFAQ6xQquaoLINyQa4QqL+cpBJFEg1dyW6GYLFSdJnx7ycQvFYllmOpGZmdPLny62GvrCWvkiIARUsmc9fpkpTx5UQEDTgmhwdCKBkhHVsx2AiQbYDxZ5WBuU1GZeiJjPuzSxvzWP6VoQBsfpwTI5kdJs6aQCekGO2/YScD+tGgrm2J89Pc/axPcb1xZzsi5SxpWhfeabQ=="
}

请求签名与回调验签

类型谁持有私钥谁持有公钥您的操作
请求签名商户平台保存商户公钥商户私钥生成 sign 发起请求
回调验签平台商户保存平台公钥从回调参数取出 sign,按同样规则拼接其余参数后,用平台公钥解密比对

回调验签时:先取出 sign,其余非空参数按 Key ASCII 排序后拼接参数值,与公钥解密结果一致即为验签通过(详见下方代码中的 verifySign 逻辑)。


代码示例

示例说明

下方 Java 示例中的 DefaultHttpClient(Apache HttpClient 4.x)已废弃,仅作签名逻辑参考;生产环境请使用 HttpClient 5.x 或您项目中的 HTTP 客户端。

import com.google.gson.JsonObject;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;

import javax.crypto.Cipher;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * TOPPAY RSA签名工具类
 *
 * @author TOPPAY
 */
public class TOPPAYRequestUtil {

    /**
     * 生成签名
     * @param params 请求参数
     * @param privateKey 商户私钥
     * @return 签名字符串
     */
    public static String generateSign(JsonObject params, String privateKey) 
            throws InvalidKeySpecException, NoSuchAlgorithmException {
        // 1. 获取所有参数名并按ASCII排序
        List<String> paramNameList = new ArrayList<>(params.keySet());
        Collections.sort(paramNameList);

        // 2. 按排序后的顺序拼接参数值
        StringBuilder stringBuilder = new StringBuilder();
        for (String name : paramNameList) {
            if (params.get(name) != null && !params.get(name).isJsonNull()) {
                String value = params.get(name).getAsString();
                if (value != null && !value.isEmpty()) {
                    stringBuilder.append(value);
                }
            }
        }
        String strA = stringBuilder.toString();
        System.out.println("待签名字符串: " + strA);

        // 3. 使用私钥进行RSA加密
        String sign = privateEncrypt(strA, getPrivateKey(privateKey));
        return sign;
    }

    /**
     * 验证签名
     * @param params 响应参数
     * @param publicKey 平台公钥
     * @return 验证结果
     */
    public static boolean verifySign(JsonObject params, String publicKey) 
            throws InvalidKeySpecException, NoSuchAlgorithmException {
        String sign = params.remove("sign").getAsString();
        List<String> paramNameList = new ArrayList<>(params.keySet());
        Collections.sort(paramNameList);

        StringBuilder stringBuilder = new StringBuilder();
        for (String name : paramNameList) {
            if (params.get(name) != null && !params.get(name).isJsonNull()) {
                String value = params.get(name).getAsString();
                if (value != null && !value.isEmpty()) {
                    stringBuilder.append(value);
                }
            }
        }
        System.out.println("验签字符串: " + stringBuilder);

        String decryptSign = publicDecrypt(sign, getPublicKey(publicKey));
        System.out.println("解密签名: " + decryptSign);

        return stringBuilder.toString().equals(decryptSign);
    }

    /**
     * 私钥加密
     * @param data 待加密数据
     * @param privateKey RSA私钥
     * @return Base64编码的加密结果
     */
    public static String privateEncrypt(String data, RSAPrivateKey privateKey) {
        try {
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.ENCRYPT_MODE, privateKey);
            return Base64.encodeBase64String(rsaSplitCodec(cipher, Cipher.ENCRYPT_MODE, 
                    data.getBytes("UTF-8"), privateKey.getModulus().bitLength()));
        } catch (Exception e) {
            throw new RuntimeException("加密字符串[" + data + "]时遇到异常", e);
        }
    }

    /**
     * 公钥解密
     * @param data Base64编码的加密数据
     * @param publicKey RSA公钥
     * @return 解密后的字符串
     */
    public static String publicDecrypt(String data, RSAPublicKey publicKey) {
        try {
            Cipher cipher = Cipher.getInstance("RSA");
            cipher.init(Cipher.DECRYPT_MODE, publicKey);
            return new String(rsaSplitCodec(cipher, Cipher.DECRYPT_MODE, 
                    Base64.decodeBase64(data), publicKey.getModulus().bitLength()), "UTF-8");
        } catch (Exception e) {
            throw new RuntimeException("解密字符串[" + data + "]时遇到异常", e);
        }
    }

    /**
     * 获取RSA私钥对象
     * @param privateKey Base64编码的私钥字符串
     * @return RSA私钥对象
     */
    public static RSAPrivateKey getPrivateKey(String privateKey) 
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey));
        RSAPrivateKey key = (RSAPrivateKey) keyFactory.generatePrivate(pkcs8KeySpec);
        return key;
    }

    /**
     * 获取RSA公钥对象
     * @param publicKey Base64编码的公钥字符串
     * @return RSA公钥对象
     */
    public static RSAPublicKey getPublicKey(String publicKey) 
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey));
        RSAPublicKey key = (RSAPublicKey) keyFactory.generatePublic(x509KeySpec);
        return key;
    }

    /**
     * RSA分段加解密
     * @param cipher 加密器
     * @param opmode 操作模式(加密/解密)
     * @param datas 待处理数据
     * @param keySize 密钥大小
     * @return 处理结果
     */
    private static byte[] rsaSplitCodec(Cipher cipher, int opmode, byte[] datas, int keySize) {
        int maxBlock = 0;
        if (opmode == Cipher.DECRYPT_MODE) {
            maxBlock = keySize / 8;
        } else {
            maxBlock = keySize / 8 - 11;
        }
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int offSet = 0;
        byte[] buff;
        int i = 0;
        try {
            while (datas.length > offSet) {
                if (datas.length - offSet > maxBlock) {
                    buff = cipher.doFinal(datas, offSet, maxBlock);
                } else {
                    buff = cipher.doFinal(datas, offSet, datas.length - offSet);
                }
                out.write(buff, 0, buff.length);
                i++;
                offSet = i * maxBlock;
            }
        } catch (Exception e) {
            throw new RuntimeException("加解密阀值为[" + maxBlock + "]的数据时发生异常", e);
        }
        byte[] resultDatas = out.toByteArray();
        IOUtils.closeQuietly(out);
        return resultDatas;
    }

    /**
     * 发送POST请求
     * @param url 请求地址
     * @param json JSON请求体
     * @return 响应结果
     */
    public static String doPost(String url, String json) throws IOException {
        HttpClient client = new DefaultHttpClient();
        HttpPost post = new HttpPost(url);
        StringEntity s = new StringEntity(json);
        s.setContentEncoding("UTF-8");
        s.setContentType("application/json");
        post.setEntity(s);
        HttpResponse res = client.execute(post);
        if (res.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
            return EntityUtils.toString(res.getEntity());
        }
        return null;
    }
}

@tab PHP

<?php
/**
 * TOPPAY RSA签名工具类
 * 
 * @author TOPPAY
 */
class TOPPAYRequestUtil
{
    /**
     * 生成签名
     * @param array $params 请求参数
     * @param string $privateKey 商户私钥
     * @return string 签名字符串
     */
    public static function generateSign(array $params, string $privateKey): string
    {
        // 1. 获取所有参数名并按ASCII排序
        $keys = array_keys($params);
        sort($keys, SORT_STRING);
        
        // 2. 按排序后的顺序拼接参数值
        $strA = '';
        foreach ($keys as $key) {
            $value = $params[$key];
            if ($value !== null && $value !== '') {
                $strA .= $value;
            }
        }
        
        // 3. 使用私钥进行RSA加密
        $privateKeyResource = openssl_pkey_get_private(
            "-----BEGIN PRIVATE KEY-----\n" . 
            wordwrap($privateKey, 64, "\n", true) . 
            "\n-----END PRIVATE KEY-----"
        );
        
        $encrypted = '';
        $data = $strA;
        $keyDetails = openssl_pkey_get_details($privateKeyResource);
        $maxBlockSize = $keyDetails['bits'] / 8 - 11;
        
        $output = '';
        while (strlen($data) > 0) {
            $chunk = substr($data, 0, $maxBlockSize);
            $data = substr($data, $maxBlockSize);
            openssl_private_encrypt($chunk, $encrypted, $privateKeyResource);
            $output .= $encrypted;
        }
        
        return base64_encode($output);
    }
    
    /**
     * 验证签名
     * @param array $params 响应参数
     * @param string $publicKey 平台公钥
     * @return bool 验证结果
     */
    public static function verifySign(array $params, string $publicKey): bool
    {
        $sign = $params['sign'];
        unset($params['sign']);
        
        // 获取所有参数名并按ASCII排序
        $keys = array_keys($params);
        sort($keys, SORT_STRING);
        
        // 按排序后的顺序拼接参数值
        $strA = '';
        foreach ($keys as $key) {
            $value = $params[$key];
            if ($value !== null && $value !== '') {
                $strA .= $value;
            }
        }
        
        // 使用公钥解密签名
        $publicKeyResource = openssl_pkey_get_public(
            "-----BEGIN PUBLIC KEY-----\n" . 
            wordwrap($publicKey, 64, "\n", true) . 
            "\n-----END PUBLIC KEY-----"
        );
        
        $encryptedData = base64_decode($sign);
        $keyDetails = openssl_pkey_get_details($publicKeyResource);
        $maxBlockSize = $keyDetails['bits'] / 8;
        
        $decrypted = '';
        while (strlen($encryptedData) > 0) {
            $chunk = substr($encryptedData, 0, $maxBlockSize);
            $encryptedData = substr($encryptedData, $maxBlockSize);
            $decryptedChunk = '';
            openssl_public_decrypt($chunk, $decryptedChunk, $publicKeyResource);
            $decrypted .= $decryptedChunk;
        }
        
        return $strA === $decrypted;
    }
    
    /**
     * 发送POST请求
     * @param string $url 请求地址
     * @param array $data 请求数据
     * @return string|false 响应结果
     */
    public static function doPost(string $url, array $data)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json',
            'Accept: application/json'
        ]);
        
        $response = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);
        
        if ($httpCode === 200) {
            return $response;
        }
        return false;
    }
}

// 使用示例
$params = [
    'mchNo' => 'PHOT000001',
    'method' => 'GCASH',
    'orderNum' => 'T1642592278863',
    'amount' => 10000,
    'productDetail' => 'Test Pay',
    'downNotifyUrl' => 'your notify url',
    'timestamp' => '1749451858772',
    'customerName' => 'JackMa',
    'expiryPeriod' => 1440,
    'customerEmail' => '[email protected]'
];

$privateKey = 'YOUR_PRIVATE_KEY_HERE';
$sign = TOPPAYRequestUtil::generateSign($params, $privateKey);
$params['sign'] = $sign;

// 发送请求
$response = TOPPAYRequestUtil::doPost('https://gateway.TOPPAY.com/v2.0/transaction/pay-in', $params);

:::


安全与排错建议

实现注意

常见对接错误

  • 签名包含 sign:计算签名前必须去掉 sign
  • 顺序错误:必须按 Key 的 ASCII 排序,不是按业务含义或文档表格顺序。
  • 多空格/换行:参数值首尾空格会参与拼接,导致与后台不一致。
  • 编码:待签名字符串使用 UTF-8
  • 改包重签:修改请求体任意字段后须重新计算 sign

常见问题

现象或问题处理建议
签名不匹配打印 StrA 与文档示例逐步对比;确认 Key 排序、仅拼接值、无分隔符
时间戳无效同步 NTP;timestamp 须为 13 位毫秒级 Unix 时间戳
密钥格式错误私钥为 PKCS#8;粘贴时保留完整 Base64,无多余换行(按示例 PEM 包装)
空值null、空字符串、缺省字段均不参与签名;不要对「占位空串」参与拼接
Java 中数字类型JsonObject 若用 getAsString(),请保证数字在 JSON 中可按字符串读取,或统一先序列化为字符串再签名

如需英文对照,请参阅 英文版签名规则