如何签名
签名规则
- 按照 ASCII 码升序(字典序)对参数进行排序
- 使用"="连接参数名和参数值
- 使用"&"连接参数对
- 使用 SHA256withRSA 算法进行签名
- 使用 Base64 对签名结果进行编码
签名示例
简单参数示例
// 原始参数
{
"amount": "100",
"currency": "USDT",
"nonce": "202402241530",
"outTradeNo": "TEST123456",
"timestamp": "1708752612"
}
// 按字典序排序并拼接
amount=100¤cy=USDT&nonce=202402241530&outTradeNo=TEST123456×tamp=1708752612
// 使用商户私钥签名,获取 Base64 编码的签名结果
sign=Base64(SHA256withRSA(merchant_private_key, sorted_string))
包含嵌套对象的复杂参数示例
当参数包含嵌套对象(如 extra
字段)时,需要先将其转换为 JSON 字符串,然后进行排序和拼接。
// 原始参数
{
"payChannel": "payChannelName",
"amount": "20",
"currency": "USDH",
"currencyId": "USDH",
"timestamp": "1754981843",
"timeExpire": "900",
"extra": {
"channel_pay_type": "cards"
}
}
// 排序拼接后的字符串
amount=20¤cy=USDH¤cyId=USDH&extra={"channel_pay_type":"cards"}&outTradeNo=1757313174350770800&payChannel=payChannelName&timeExpire=900×tamp=1754981843
// 最终签名请求
{
"payChannel": "payChannelName",
"sign": "i4vN6MpFF1fe1KeEUpUreNMSpk7ac9MWclrDJvgptUJ4eyQXF3vbmSfgEZZBqQoz9aEom3EkaEW9iLbGFhY2vzK8oqr9NRcDEOmjNzwnwJHZp+L6NzKVgc/2piRCMpH0sUH/vTJpn0fqJX1xMvucaclVQB/dMWXT4NgoRujdjXk=",
"outTradeNo": "1757313174350770800",
"amount": "20",
"currency": "USDH",
"currencyId": "USDH",
"timestamp": "1754981843",
"timeExpire": "900",
"payAddress": "",
"extra": {
"channel_pay_type": "cards"
}
}
重要说明:当 extra
字段包含多个参数时,extra
对象内的键名也必须按照 ASCII 顺序排序后再进行 JSON 序列化。
包含多个 Extra 参数的示例
// 原始参数
{
"payChannel": "payChannelName",
"amount": "1.5",
"currency": "USDT",
"currencyId": "USDT",
"timestamp": "1757913914",
"payAddress": "+855-xxxxxxxx",
"outTradeNo": "78988784565456",
"extra": {
"channel_pay_type": "card",
"description": "edison",
"attach": "edison"
}
}
// 步骤 1:按 ASCII 顺序对 extra 对象内的键进行排序
// attach -> channel_pay_type -> description
// 然后转换为 JSON 字符串
{
"payChannel": "payChannelName",
"amount": "1.5",
"currency": "USDT",
"currencyId": "USDT",
"timestamp": "1757913914",
"payAddress": "+855-xxxxxxxx",
"outTradeNo": "78988784565456",
"extra": "{\"attach\":\"edison\",\"channel_pay_type\":\"card\",\"description\":\"edison\"}"
}
// 步骤 2:按 ASCII 顺序对所有参数进行排序并拼接
amount=1.5¤cy=USDT¤cyId=USDT&extra={"attach":"edison","channel_pay_type":"card","description":"edison"}&outTradeNo=78988784565456&payAddress=+855-xxxxxxxx&payChannel=payChannelName×tamp=1757913914
// 步骤 3:使用商户私钥签名
sign=Base64(SHA256withRSA(merchant_private_key, sorted_string))
golang sdk
package service
import (
"bytes"
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"sort"
"strings"
"time"
)
type SignSevice struct {
Appid string
PrivateKey string
PublicKey string
}
func NewSignSevice() *SignSevice {
return &SignSevice{}
}
func (p *SignSevice) parsePrivateKey(prikey string) (*rsa.PrivateKey, error) {
block, _ := pem.Decode([]byte(prikey))
if block == nil || block.Type != "PRIVATE KEY" {
return nil, errors.New("failed to parse private key")
}
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse private key failed: %v", err)
}
// 类型断言为 *rsa.PrivateKey
rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("private key type is not rsa")
}
return rsaPrivateKey, nil
}
func (p *SignSevice) formatPublicKey(publicKey string) string {
const lineLength = 64
var b strings.Builder
b.WriteString("-----BEGIN PUBLIC KEY-----\n")
for len(publicKey) > 0 {
chunk := publicKey
if len(chunk) > lineLength {
chunk = publicKey[:lineLength]
publicKey = publicKey[lineLength:]
} else {
publicKey = ""
}
b.WriteString(chunk)
b.WriteByte('\n')
}
b.WriteString("-----END PUBLIC KEY-----\n")
return b.String()
}
func (p *SignSevice) parsePublicKey(key string) (*rsa.PublicKey, error) {
formattedPubKey := p.formatPublicKey(key)
block, _ := pem.Decode([]byte(formattedPubKey))
if block == nil {
return nil, errors.New("public key is invalid")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parse public key failed: %v", err)
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, errors.New("public key type is not rsa")
}
return rsaPub, nil
}
// 排序并拼接参数
func (p *SignSevice) sortAndConcatenate(params map[string]interface{}) string {
var keys []string
for key := range params {
if key != "sign" && params[key] != "" && params[key] != nil {
keys = append(keys, key)
}
}
// 按 ASCII 排序
sort.Strings(keys)
// 拼接字段
var sb strings.Builder
for _, key := range keys {
sb.WriteString(key + "=")
strVal := ""
strVal = fmt.Sprintf("%v&", params[key])
sb.WriteString(strVal)
}
return strings.TrimRight(sb.String(), "&")
}
// 生成签名
func (p *SignSevice) GenerateSignature(prikey string, params map[string]any) (string, error) {
privateKey, err := p.parsePrivateKey(prikey)
if err != nil {
return "", err
}
// 拼接参数
concatenatedParams := p.sortAndConcatenate(params)
// SHA256 哈希
hash := sha256.New()
hash.Write([]byte(concatenatedParams))
hashBytes := hash.Sum(nil)
// 签名
signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashBytes)
if err != nil {
return "", err
}
// 转换为 Base64 编码
return base64.StdEncoding.EncodeToString(signature), nil
}
多语言实现
处理嵌套对象
当处理包含嵌套对象(如 extra
字段)的复杂参数时,不同编程语言需要一致地处理 JSON 序列化。
JavaScript/Node.js 示例
const crypto = require('crypto');
function handleNestedObjects(params) {
const processedParams = {};
for (const [key, value] of Object.entries(params)) {
if (key === 'sign') continue;
if (typeof value === 'object' && value !== null) {
// 将嵌套对象转换为 JSON 字符串
processedParams[key] = JSON.stringify(value);
} else {
processedParams[key] = String(value);
}
}
return processedParams;
}
function generateSignature(params, privateKey) {
// 步骤 1: 处理嵌套对象
const processedParams = handleNestedObjects(params);
// 步骤 2: 排序并拼接
const sortedKeys = Object.keys(processedParams).sort();
const queryString = sortedKeys
.map(key => `${key}=${processedParams[key]}`)
.join('&');
// 步骤 3: 使用 RSA-SHA256 签名
const sign = crypto.createSign('RSA-SHA256');
sign.update(queryString);
return sign.sign(privateKey, 'base64');
}
// 使用示例
const params = {
"amount": "0.01",
"payChannel": "payway",
"currency": "USD",
"currencyId": "USD",
"extra": {
"channel_pay_type": "cards"
}
};
const signature = generateSignature(params, privateKey);
Python 示例
import json
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from collections import OrderedDict
def handle_nested_objects(params):
processed_params = {}
for key, value in params.items():
if key == 'sign':
continue
if isinstance(value, dict):
# 将嵌套对象内的键按 ASCII 顺序排序后再转换为 JSON 字符串
sorted_dict = OrderedDict(sorted(value.items()))
processed_params[key] = json.dumps(sorted_dict, separators=(',', ':'))
else:
processed_params[key] = str(value)
return processed_params
def generate_signature(params, private_key_pem):
# 步骤 1: 处理嵌套对象
processed_params = handle_nested_objects(params)
# 步骤 2: 排序并拼接
sorted_keys = sorted(processed_params.keys())
query_string = '&'.join([f"{key}={processed_params[key]}" for key in sorted_keys])
# 步骤 3: 加载私钥并签名
private_key = serialization.load_pem_private_key(
private_key_pem.encode(),
password=None
)
signature = private_key.sign(
query_string.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
return base64.b64encode(signature).decode()
# 使用示例
params = {
"amount": "0.01",
"payChannel": "payway",
"currency": "USD",
"currencyId": "USD",
"extra": {
"channel_pay_type": "cards"
}
}
signature = generate_signature(params, private_key_pem)
Java 示例
import com.fasterxml.jackson.databind.ObjectMapper;
import java.security.PrivateKey;
import java.security.Signature;
import java.util.*;
import java.util.stream.Collectors;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class SignatureUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
public static Map<String, String> handleNestedObjects(Map<String, Object> params) {
Map<String, String> processedParams = new HashMap<>();
for (Map.Entry<String, Object> entry : params.entrySet()) {
String key = entry.getKey();
Object value = entry.getValue();
if ("sign".equals(key)) {
continue;
}
if (value instanceof Map || value instanceof List) {
try {
// 将嵌套对象转换为 JSON 字符串
processedParams.put(key, objectMapper.writeValueAsString(value));
} catch (Exception e) {
throw new RuntimeException("序列化嵌套对象失败", e);
}
} else {
processedParams.put(key, String.valueOf(value));
}
}
return processedParams;
}
public static String generateSignature(Map<String, Object> params, PrivateKey privateKey) {
try {
// 步骤 1: 处理嵌套对象
Map<String, String> processedParams = handleNestedObjects(params);
// 步骤 2: 排序并拼接
String queryString = processedParams.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.map(entry -> entry.getKey() + "=" + entry.getValue())
.collect(Collectors.joining("&"));
// 步骤 3: 使用 RSA-SHA256 签名
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(queryString.getBytes(StandardCharsets.UTF_8));
byte[] signatureBytes = signature.sign();
return Base64.getEncoder().encodeToString(signatureBytes);
} catch (Exception e) {
throw new RuntimeException("生成签名失败", e);
}
}
}
跨语言兼容性要点
- JSON 序列化: 将嵌套对象转换为字符串时,始终使用紧凑的 JSON 格式(无空格)
- 参数排序: 使用 ASCII/字典序对参数键名进行排序
- 编码: 所有字符串操作使用 UTF-8 编码
- 签名算法: 使用 SHA256withRSA(PKCS#1 v1.5 填充)
- Base64 编码: 对最终签名使用标准 Base64 编码
常见陷阱避免
- 不一致的 JSON 格式: 不同语言可能为同一对象生成不同的 JSON 字符串
- 参数过滤: 始终排除
sign
参数和空/null 值 - 字符编码: 确保一致使用 UTF-8 编码
- 键排序: 使用区分大小写的 ASCII 排序,而非特定区域设置的排序