签名&验签算法

# 密钥说明

  • 密钥算法:RSA
  • RSA密钥格式:pkcs#8
  • RSA密钥长度:2048 bit

# 密钥生成

# 密钥使用

  • 商户私钥,对请求参数进行签名,签名值放入sign值中,请求时一起提交给CheezeePay
  • 商户公钥,请查看商户公钥配置说明

# 签名算法说明

  • 签名算法:SHA256WithRSA,Java对应算法枚举类型:SHA256,PHP对应算法枚举类型:OPENSSL_ALGO_SHA256

# 加签&验签字符串获取步骤

  1. 获取请求参数内容,剔除 sign 字段,剔除值为空的参数;
  2. 按照第一个字符的键值 ASCII 码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值 ASCII 码递增排序,以此类推;
  3. 将排序后的参数与其对应值,组合成 参数=参数值 的格式,并且把这些参数用 & 字符连接起来,此时生成的字符串为待签名字符串。

# 加签示例

# 参数示例值

{
    "appId": "B2iwKFIM4tvo",
    "merchantId": "CH10001165",
    "mchOrderNo": "C2024010901015",
    "paymentMode": "LOCAL_PAY",
    "amount": "800",
    "name": "CheezeePayTest",
    "phone": "9123456789",
    "email": "[email protected]",
    "notifyUrl": "http://www.domain.com/notify",
    "returnUrl": "www.baidu.com",
    "language": "zh_hk",
    "timestamp": "1704167614000"
}

# 拼接后的待签名字符串

amount=800&appId=B2iwKFIM4tvo&email=9123456789@qq.com&language=zh_hk&mchOrderNo=C2024010901015&merchantId=CH10001165&name=CheezeePayTest&notifyUrl=your notify url&paymentMode=LOCAL_PAY&phone=9123456789&returnUrl=www.baidu.com&timestamp=1704167614000

# 生成签名

使用您生成的商户私钥进行加签获取到最终的签名字符串。

# 验证签名

使用您在CheezeePay商户后台获取的平台公钥对sign进行验签。

# 签名工具Demo

package com.cheesepay;

import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.*;

public class CheeseTradeRSAUtil {
    private static final String DATA = "data";

    // 生成签名
    public static String getSign(Map<String, Object> params, String privateKey) throws Exception {
        params.remove("sign");
        String textContent = getContent(params);
        return sign(textContent, privateKey);
    }

    // 验证签名
    public static boolean verifySign(Map<String, Object> params, String publickey) throws
        Exception {
        String platSign= (String)params.remove("sign");

        String textContent = getContent(params);
        return verify(textContent, platSign, publickey);
    }

    // 拼接待签名的字符串
    private static String getContent(Map<String, Object> params) {
        List<String> paramNameList = new ArrayList<>(params.keySet());
        Collections.sort(paramNameList);
        StringBuilder stringBuilder = new StringBuilder();
        for (String name : paramNameList) {
            if(ObjectUtil.isNotEmpty(params.get(name))) {
                if (params.get(name) != null ) {
                    if (DATA.equals(name)){
                        String jsonString = getJsonStringByJackson(params.get(name));
                        stringBuilder.append(name).append("=").append(jsonString).append("&");
                    }else {
                        stringBuilder.append(name).append("=").append(params.get(name)).append("&");
                    }
                }
            }
        }
        String content = stringBuilder.toString();
        content = content.substring(0, content.length() - 1);
        return content;
    }
    
    // Jackson方式序列化对象
    private static String getJsonStringByJackson(Object param) {
        // 使用Jackson方式序列化对象
        ObjectMapper objectMapper = new ObjectMapper();
        try {
            return objectMapper.writeValueAsString(param);
        } catch (Exception ex) {
            return null;
        }
    }

    // 使用私钥生成签名
    public static String sign(String message, String privateKeyString) throws Exception {
        byte[] privateKeyBytes = java.util.Base64.getDecoder().decode(privateKeyString);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(privateKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(spec);
        Signature signer = Signature.getInstance("SHA256withRSA");
        signer.initSign(privateKey);
        signer.update(message.getBytes());
        byte[] signatureBytes = signer.sign();
        return java.util.Base64.getEncoder().encodeToString(signatureBytes);
    }

    // 使用公钥验签
    public static boolean verify(String message, String signatureString, String publicKeyString) throws Exception {
        byte[] publicKeyBytes = java.util.Base64.getDecoder().decode(publicKeyString);
        byte[] signatureBytes = java.util.Base64.getDecoder().decode(signatureString);
        X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKeyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicKey = keyFactory.generatePublic(spec);
        Signature verifier = Signature.getInstance("SHA256withRSA");
        verifier.initVerify(publicKey);
        verifier.update(message.getBytes());
        return verifier.verify(signatureBytes);
    }


    public static void main(String[] args) throws Exception {

        // RSA公钥
        String publicKeyString = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv8ICamtlh1E6ycHa7TooPJcLij2i+Otv9axeOafsYtubqBlBMojrQdDguhy3j1H5BmMz1Czx/ztMheRplpIUrs95Siv17V0nkXUCMAEO5WJYX3SY0y1Qu5sJA0WXhXj8G5wF+aQtIemMX9Im4wIJZFtneOQLHKLmbfdcSMYH9FSzWfXR3vEe4/ITyjhqTYvu9PU4ZkWUFR0CzfusPpqyA+yclgUm239m1VnO1AZRwpLncIxIlv6/egnn07pG9EooGktF6alhCmB3jVktAz/2uTlA81zIun6hxMzD2urjWGy6Tlta8TITIVPe1vKS2AW2tE/QSOxf8brKM8VQ1XkGwwIDAQAB";

        // RSA私钥
        String privateKeyString = "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/wgJqa2WHUTrJwdrtOig8lwuKPaL462/1rF45p+xi25uoGUEyiOtB0OC6HLePUfkGYzPULPH/O0yF5GmWkhSuz3lKK/XtXSeRdQIwAQ7lYlhfdJjTLVC7mwkDRZeFePwbnAX5pC0h6Yxf0ibjAglkW2d45AscouZt91xIxgf0VLNZ9dHe8R7j8hPKOGpNi+709ThmRZQVHQLN+6w+mrID7JyWBSbbf2bVWc7UBlHCkudwjEiW/r96CefTukb0SigaS0XpqWEKYHeNWS0DP/a5OUDzXMi6fqHEzMPa6uNYbLpOW1rxMhMhU97W8pLYBba0T9BI7F/xusozxVDVeQbDAgMBAAECggEAdZ4EaU3yemuCiZoUNIoFgBSNiX+A5PlUNPZC3U54mbJl6VeEPADre/Uowj82//uhqR9T/QKMdKbkqwONGEQF16t+k9YfBDatPHTuoI8lmeEWn4Ye7vjOmiPgBVe8NqwcxrqOl67x1+kupt955qerJxlBgE8v2aK5gB3HRwPggSYZgSZJ3X2zQMJ+XN4qe49Un+utqFuGtVRwPQvs/Kazp5GL7WlR0OnpQ2KZ+ur9T58VfTKmIeqBckey7nIrb/fdodJ7IqcykWjSLmy8p84W78iiRDfPn81Ujb231EWW+AbuV399aXBZ4oaR4q8wwkhe9xHUADWLvcfCnMrB40LkEQKBgQD2TEeBEB5Gwj/zpriq23xKJyzfo0hOxzGY/Ol2EykGeEYhny/YiTlMYtOMy37RiGKr4lN7G/pKl2fI5kRjgQM1QXgx6dxd11D0rORXj+SAy6+zAq3NVY4c3zDKmrQQ8x3f2QFffo+TmLtw/BH0Frp+a2lzKzIcXtZ4/akJdVIihwKBgQDHT7xvOaNyAPnnYwpROMI9eUVgFc9pMRAmQ18at8ggZ8M+Ww1nhr00loufzkovCb9MvGR4HSedWHfgmtDu1hUFtl7PItNItsB347yWPxR/lVLs44NVXt7jkFisoBoEBXaNVVt6uOSvBiqINmX3gm4FoisI2SRZsH7nMr07OXO85QKBgCkXdN6Nh9+aTP0jla+7yrK8cnolTc0G4rl5iKHQdInFyz2Ux3DDBDJAUrcsxE3FCWFP80mY01tag1A7SrGnNpfOAnWhZMirQbBwn/AT67WpRfkBu2mEmp8qhYNn6B7j0Nol5FrATMf9NViWVZB5skehOAik7klZcULiXy9ayYdFAoGAA+0bLim5gYZpZdh6nIz8ro9UAYeMdWsFAWv0VKdoJtNEclcC1ZY+5elNSNGcfn63qQBRDlisrhnPCrqiAQJwmZM1HOl3tgf7lKEE1wTZF4ZOguIcdTMQOvVd+dCRkqC07CXRToKT5qq9bb98lLqaBjxikqMMZ0PlzBIgzijckc0CgYAH16CziStEZVvKbiR8b0ftF9mf6V8I0KHianMgV8X6Y51aE0Ig1eQhIvqSjqtBac7gEppiu3OQVrp39uLdgyvDSVta1Hq9QYYTDNt8crt69aD/Th1h5esYvApYyZIhHbFDR5meE4Y3nHpfY7J8zhmfyaVW/6cuwPaToTtrEjQmWQ==";

        // 请求接口参数
        Map<String, Object> params  = new HashMap<>();
        params.put("a","A");
        params.put("b","BB");

        // 使用私钥生成签名
        String platSign = getSign(params, privateKeyString);
        System.out.println("sign: " + platSign);
        params.put("sign",platSign);

        // 使用公钥验证签名
        boolean verified = verifySign( params, publicKeyString);
        System.out.println("Signature verified: " + verified);

    }
}
<?php

// 生成签名
function genSign($toSign, $privateKey){
    $key = openssl_get_privatekey($privateKey);
    openssl_sign($toSign, $signature, $key, OPENSSL_ALGO_SHA256);
    openssl_free_key($key);
    $sign = base64_encode($signature);
    return $sign;
}

// 验证签名
function verifySign($data, $sign, $pubKey){
    $sign = base64_decode($sign);
    $key = openssl_pkey_get_public($pubKey);
    $result = openssl_verify($data, $sign, $key, OPENSSL_ALGO_SHA256) === 1;
    return $result;
}

function verifySign($params, $publickey) {
    $platSign = $params['sign'];
    unset($params['sign']);
    $textContent = self::getContent($params);
    return self::verify($textContent, $platSign, $publickey);
}

// 定义数据,模拟输入
$jsonString ='{"msg":"success","code":"000000","data":[{"currency":"USDT","availableBalance":"1000000925.88303","freezeBalance":"3.473683","totalBalance":"1000000929.356713"},{"currency":"INR","availableBalance":"19000121031.34","freezeBalance":"549.98","totalBalance":"19000121581.32"}],"merchantId":"CH10001165"}';
$phpArray = json_decode($jsonString, true); 

ksort($phpArray); 
$str = '';
foreach ($phpArray as $key => $value) {
     if ($key == "data") {
        $jsonString = json_encode($value);
        $str .= "{$key}={$jsonString}&";
     }else{
       $str .= "{$key}={$value}&";
     }
}
$str = rtrim($str, '&');
// 输出拼接后的字符串
echo $str;

// RSA私钥
$privateKey = "-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC/wgJqa2WHUTrJwdrtOig8lwuKPaL462/1rF45p+xi25uoGUEyiOtB0OC6HLePUfkGYzPULPH/O0yF5GmWkhSuz3lKK/XtXSeRdQIwAQ7lYlhfdJjTLVC7mwkDRZeFePwbnAX5pC0h6Yxf0ibjAglkW2d45AscouZt91xIxgf0VLNZ9dHe8R7j8hPKOGpNi+709ThmRZQVHQLN+6w+mrID7JyWBSbbf2bVWc7UBlHCkudwjEiW/r96CefTukb0SigaS0XpqWEKYHeNWS0DP/a5OUDzXMi6fqHEzMPa6uNYbLpOW1rxMhMhU97W8pLYBba0T9BI7F/xusozxVDVeQbDAgMBAAECggEAdZ4EaU3yemuCiZoUNIoFgBSNiX+A5PlUNPZC3U54mbJl6VeEPADre/Uowj82//uhqR9T/QKMdKbkqwONGEQF16t+k9YfBDatPHTuoI8lmeEWn4Ye7vjOmiPgBVe8NqwcxrqOl67x1+kupt955qerJxlBgE8v2aK5gB3HRwPggSYZgSZJ3X2zQMJ+XN4qe49Un+utqFuGtVRwPQvs/Kazp5GL7WlR0OnpQ2KZ+ur9T58VfTKmIeqBckey7nIrb/fdodJ7IqcykWjSLmy8p84W78iiRDfPn81Ujb231EWW+AbuV399aXBZ4oaR4q8wwkhe9xHUADWLvcfCnMrB40LkEQKBgQD2TEeBEB5Gwj/zpriq23xKJyzfo0hOxzGY/Ol2EykGeEYhny/YiTlMYtOMy37RiGKr4lN7G/pKl2fI5kRjgQM1QXgx6dxd11D0rORXj+SAy6+zAq3NVY4c3zDKmrQQ8x3f2QFffo+TmLtw/BH0Frp+a2lzKzIcXtZ4/akJdVIihwKBgQDHT7xvOaNyAPnnYwpROMI9eUVgFc9pMRAmQ18at8ggZ8M+Ww1nhr00loufzkovCb9MvGR4HSedWHfgmtDu1hUFtl7PItNItsB347yWPxR/lVLs44NVXt7jkFisoBoEBXaNVVt6uOSvBiqINmX3gm4FoisI2SRZsH7nMr07OXO85QKBgCkXdN6Nh9+aTP0jla+7yrK8cnolTc0G4rl5iKHQdInFyz2Ux3DDBDJAUrcsxE3FCWFP80mY01tag1A7SrGnNpfOAnWhZMirQbBwn/AT67WpRfkBu2mEmp8qhYNn6B7j0Nol5FrATMf9NViWVZB5skehOAik7klZcULiXy9ayYdFAoGAA+0bLim5gYZpZdh6nIz8ro9UAYeMdWsFAWv0VKdoJtNEclcC1ZY+5elNSNGcfn63qQBRDlisrhnPCrqiAQJwmZM1HOl3tgf7lKEE1wTZF4ZOguIcdTMQOvVd+dCRkqC07CXRToKT5qq9bb98lLqaBjxikqMMZ0PlzBIgzijckc0CgYAH16CziStEZVvKbiR8b0ftF9mf6V8I0KHianMgV8X6Y51aE0Ig1eQhIvqSjqtBac7gEppiu3OQVrp39uLdgyvDSVta1Hq9QYYTDNt8crt69aD/Th1h5esYvApYyZIhHbFDR5meE4Y3nHpfY7J8zhmfyaVW/6cuwPaToTtrEjQmWQ==
-----END PRIVATE KEY-----
";
// RSA公钥
$pubKey = "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv8ICamtlh1E6ycHa7TooPJcLij2i+Otv9axeOafsYtubqBlBMojrQdDguhy3j1H5BmMz1Czx/ztMheRplpIUrs95Siv17V0nkXUCMAEO5WJYX3SY0y1Qu5sJA0WXhXj8G5wF+aQtIemMX9Im4wIJZFtneOQLHKLmbfdcSMYH9FSzWfXR3vEe4/ITyjhqTYvu9PU4ZkWUFR0CzfusPpqyA+yclgUm239m1VnO1AZRwpLncIxIlv6/egnn07pG9EooGktF6alhCmB3jVktAz/2uTlA81zIun6hxMzD2urjWGy6Tlta8TITIVPe1vKS2AW2tE/QSOxf8brKM8VQ1XkGwwIDAQAB
-----END PUBLIC KEY-----";

// 生成签名
$sign = genSign($str, $privateKey);

// 校验签名
$rs = verifySign($str, $sign, $pubKey);
var_dump($sign);
var_dump($rs);
const CRYPTO = require('crypto');

//签名(请求参数,商户私钥)
function getSign(params, privateKeyString) {
    try {
        const message = getContent(params);
        console.log('Generated string:', message);
        const signer = CRYPTO.createSign('RSA-SHA256');
        signer.update(message);

        const signature = signer.sign(privateKeyString, 'base64');
        return signature;
    } catch (error) {
        throw new Error(`签名失败: ${error.message}`);
    }
}

// 验签函数(响应体,平台公钥)
function verifySign(responseBody,platPublicKey) {
    try {
        const signature = responseBody.sign;
        delete responseBody.sign;
        // 解码签名数据
        const signatures = Buffer.from(signature, 'base64');
        // 创建验签对象
        const verifier = CRYPTO.createVerify('RSA-SHA256');
        const dataToVerify= getContent(responseBody);
        verifier.update(dataToVerify);

        // 使用公钥验证签名
        const isValid = verifier.verify(platPublicKey, signatures);
        return isValid;
    } catch (error) {
        console.error('验签失败:', error.message);
        return false;
    }
}

//拼接请求参数
function getContent(params) {
    // 获取参数名的列表并排序
    const paramNameList = Object.keys(params).sort();

    // 用于拼接结果的数组
    const resultParts = [];

    for (const name of paramNameList) {
        const value = params[name];

        // 检查值是否不为空
        if (value != null) {
            if (name === 'DATA' || name === 'data') {
                // 假设 getJsonStringByJackson 是一个将对象转换为 JSON 字符串的函数
                const jsonString = getJsonStringByJackson(value);
                resultParts.push(`${name}=${jsonString}`);
            } else {
                resultParts.push(`${name}=${value}`);
            }
        }
    }

    // 将数组拼接成字符串,并用 "&" 连接
    const content = resultParts.join('&');
    return content;
}

function getJsonStringByJackson(value) {
    try {
        // 使用 JSON.stringify 将对象转换为 JSON 字符串
        const jsonString = JSON.stringify(value);
        return jsonString;
    } catch (error) {
        // 如果转换失败,抛出错误
        throw new Error(`Failed to convert object to JSON string: ${error.message}`);
    }
}

module.exports = { getSign, verifySign };