签名授权
大约 6 分钟
数字签名概述
数字签名认证是所有 API 交易(包括代收、代付以及查询操作(查询状态和查询余额))的关键安全机制。该流程通过加密验证确保请求的完整性、真实性和不可否认性。
适用于所有需签名的 API
每个代收、代付和查询请求都必须包含 X-TIMESTAMP、X-SIGNATURE 和 X-PARTNER-ID 请求头。本页的签名规则与代收请求参考文档保持一致。
签名生成流程
准备签名数据
收集以下用于生成签名的组成部分:
- X-TIMESTAMP:ISO 8601 格式的时间戳(例如
2024-12-30T18:30:36+07:00) - Merchant Secret:分配给您的商户密钥
- Request Body:JSON 请求体(将被压缩)
- Merchant Private Key:用于签名的 RSA 私钥(PKCS#8,Base64)

压缩请求体
移除 JSON 请求体中所有不必要的空白和格式,以确保签名生成的一致性。
{
"area": 10,
"callbackUrl": "https://docs.smilepayz.com/en/",
"merchant": {
"merchantId": "20019"
},
"money": {
"amount": 10000,
"currency": "IDR"
},
"orderNo": "20019c5b63c4b-e34a-4855-9b20-d4b",
"paymentMethod": "QRIS",
"purpose": "Purpose For Transaction from Java SDK"
}
{"area":10,"callbackUrl":"https://docs.smilepayz.com/en/","merchant":{"merchantId":"20019"},"money":{"amount":10000,"currency":"IDR"},"orderNo":"20019c5b63c4b-e34a-4855-9b20-d4b","paymentMethod":"QRIS","purpose":"Purpose For Transaction from Java SDK"}
签名后请勿修改请求体
请使用将在 HTTP 请求体中发送的、完全一致的压缩 JSON 字符串来生成签名。签名后的任何改动都会导致验证失败。
拼接待签名字符串
使用竖线(|)分隔符拼接各组成部分,构造待签名字符串:
X-TIMESTAMP + "|" + merchant_secret + "|" + minify(requestBody)
待签名字符串示例
2024-12-30T18:30:36+07:00|95b57c46b8c2e068982be23fb669a80612cad68e6ce6ba4f5af9ec20d23bb274|{"area":10,"callbackUrl":"https://docs.smilepayz.com/en/","merchant":{"merchantId":"20019"},"money":{"amount":10000,"currency":"IDR"},"orderNo":"20019c5b63c4b-e34a-4855-9b20-d4b","paymentMethod":"QRIS","purpose":"Purpose For Transaction from Java SDK"}
生成数字签名
使用您的 RSA 私钥对待签名字符串应用 SHA256withRSA 算法。
加密公式
SHA256withRSA(stringToSign, merchantPrivateKey)
RSA 私钥示例(PKCS#8,Base64)
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC1d8Zaurk/A0WB1X7CrW+SqsVM4v2ZMGTdqWEQOAKQyY/DZEmgSm7whUMLYfqeKxzzAd3lZHvviyO/V/Ht2i8axtiA5tyDa485IgBy+jO9UHxzI+9CRxnIY5jjDAqAgEvRa0HZEfYHLGlH3IQ4A2a7g8Wm3yqn2prIrgY21nO4+33GuBR0qCpGxth0kV7/RSdhWv3Ff1zkb7yUYn9zfuag9stLf1nw+Ui7hjXL+xGiNRYsDCHOLzKynZxk1cBfqyPY5YMTIjQik4cI+YfM4BMN8bmTB1y3z9sdfbHVTI/PD/fbDzvfK7S/XJvY2bj1TwNeYCHcd5Dx6MYiO12rfA5XAgMBAAECggEASRpMJMat042c1ZXM793wYXQ78SJMKQcOyyDtwhveJLeeG4duBY2WrXvxq0c4L7mMevSYkE1vg+tYZj+mbTwE19Oc6iiWUrbkxo5FAgx7BVvEFpo2S0vbPhepQmXaYTPN1zpPF4QdyXwBmXpjJTFxlD6cRAII6/+rdQZt6G9xxOWUq6bbENNUc6y+N+97oPPwrzLZGYADfusM735Mlx3j8xTGLSHTh2d8e5epwKcSW+ZvBbiM30ZeO+M5bh6zgZrHpPQNJAOsEDuVk5vjuC5O+i81YwP702GAE50zI9Ysr4QMS3QQGe2jwpJAmMjvO6/MRv/6De83NJ6Y70+00OONcQKBgQDtP+sQOyKRErCN9ePmu6FBfzMZcTC5vXQpA0kW7vUMKyQR537appYcOb5DPJDgUqqyNcr8vIqadR6OW0thkfRyb+L/elOyS/XnVbFc6RA9DW/KV4xm68LUX9oRqfgy5YOESEEA2igEh8IdqfkIRCZ3g7xrQUmb3gq0BM2/hnxyHwKBgQDDz0Slbw2Pv820CSheZi8HQf3sEFe0JDHKo2KAHX9rTvFEF3xsT+UZh6pcHUcesYlVlJM2Ciejq4dmiOnvGIKtkqtcLfioVbPrDP2HlDtIT3mSC7sL6VdWLxYz/l0EVjsR2KMVwXD42jcjz3e5bDIi98Z8G4Bx6cu6pwlBtfMMyQKBgHJhWoacu/GNPSlz3sgitX/KP2yIsaEawaH0P4ya7/FJ6z3mibYIkl5RSHVKAd7ke/8nCBIJJBmLVYv0X8dOgreZUasx4qAXFxrMqZHNm1KT4819n+cOywNwosXZWBvRozJnU0+B3cExYljC1ZkyogkErhZXh5Fp32ci2b74q46pAoGANWyixSRkpdjmLf3lpsPohulpd3QKCdtqPmiPonbp4gRa7YIe74po7qtGPvW6BtTXrKLp+2+P2yccvUwTz6l3VhZTbMYaNwuTGEmmDszR0+vjNoa+1QQoURpty2fWRy2j1j2uWWw6/5rCOqILf2rWzxWdcRUr2Wi5rkaOzv/uvYkCgYEAlFV6huNhi1NIIg5gkTS0kmxnIUWu8XIT9IgZWMrhR7TvImVISoormE9LdISgMBz+J5YJxhAZbxVxlUJrsl+HVcS81gmUkeoIpTA+iKUzbD9gAPjEdqVylzN8A9uNIkkb5Cf4BWYfEyMaLfBuiHDlKDcBjz4cJbcom3RVGacghKc=
生成的签名示例
BD+qFc0/JOl1grDKSs0zMumPeswzLN/tVj/bms9AYZdVwtzZ3RQJ2gdCxtMeF/fYVTLOZZVLiKnFm0KDjoChl9Kz+6VpgFCMCQt99iUxoJxLtCvPhlCdD1rHVNfrNWKt4mLSNdQWmh//hlwP4AJqidMUKCTDMDm2ivgvQqMIFqdadITAreevKQ+VZs7ukZMzo5BgYfdhCiorAqAb/EUN1/O0pDt8Uv+xcL4QNxxu1dj10UDRbHaox4Qv1tfXbRoiisNa9xAlVkd3l19FW/Mv09jkDEKL+x5uIx3WjGY81NyVW6yxx5jEbsZbdl5KfqA6SwKpc3IOVLwR4J4Q0jaFGA==
代码示例
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
public class SignatureExample {
/** Build X-SIGNATURE for Smilepayz APIs. */
public static String sign(
String timestamp,
String merchantSecret,
String minifiedRequestBody,
String privateKeyBase64) throws Exception {
String stringToSign = timestamp + "|" + merchantSecret + "|" + minifiedRequestBody;
byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(stringToSign.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
}
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
def sign(timestamp: str, merchant_secret: str, minified_body: str, private_key_base64: str) -> str:
"""Build X-SIGNATURE for Smilepayz APIs."""
string_to_sign = f"{timestamp}|{merchant_secret}|{minified_body}"
key_bytes = base64.b64decode(private_key_base64)
private_key = serialization.load_der_private_key(key_bytes, password=None)
signature = private_key.sign(
string_to_sign.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256(),
)
return base64.b64encode(signature).decode("utf-8")
const crypto = require("crypto");
/**
* Build X-SIGNATURE for Smilepayz APIs.
* @param {string} timestamp - X-TIMESTAMP value
* @param {string} merchantSecret - merchant secret
* @param {string} minifiedBody - minified JSON request body
* @param {string} privateKeyBase64 - PKCS#8 private key (Base64)
*/
function sign(timestamp, merchantSecret, minifiedBody, privateKeyBase64) {
const stringToSign = `${timestamp}|${merchantSecret}|${minifiedBody}`;
const privateKey = crypto.createPrivateKey({
key: Buffer.from(privateKeyBase64, "base64"),
format: "der",
type: "pkcs8",
});
const signer = crypto.createSign("RSA-SHA256");
signer.update(stringToSign, "utf8");
return signer.sign(privateKey, "base64");
}
<?php
/**
* Build X-SIGNATURE for Smilepayz APIs.
*/
function sign(
string $timestamp,
string $merchantSecret,
string $minifiedBody,
string $privateKeyBase64
): string {
$stringToSign = $timestamp . '|' . $merchantSecret . '|' . $minifiedBody;
$keyPem = "-----BEGIN PRIVATE KEY-----\n"
. chunk_split($privateKeyBase64, 64, "\n")
. "-----END PRIVATE KEY-----\n";
$privateKey = openssl_pkey_get_private($keyPem);
openssl_sign($stringToSign, $signature, $privateKey, OPENSSL_ALGO_SHA256);
return base64_encode($signature);
}
using System;
using System.Security.Cryptography;
using System.Text;
public static class SignatureExample
{
/// <summary>Build X-SIGNATURE for Smilepayz APIs.</summary>
public static string Sign(
string timestamp,
string merchantSecret,
string minifiedBody,
string privateKeyBase64)
{
var stringToSign = $"{timestamp}|{merchantSecret}|{minifiedBody}";
var keyBytes = Convert.FromBase64String(privateKeyBase64);
using var rsa = RSA.Create();
rsa.ImportPkcs8PrivateKey(keyBytes, out _);
var data = Encoding.UTF8.GetBytes(stringToSign);
var signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
return Convert.ToBase64String(signature);
}
}
package signature
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
)
// Sign builds X-SIGNATURE for Smilepayz APIs.
func Sign(timestamp, merchantSecret, minifiedBody, privateKeyBase64 string) (string, error) {
stringToSign := timestamp + "|" + merchantSecret + "|" + minifiedBody
keyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
if err != nil {
return "", err
}
privateKey, err := x509.ParsePKCS8PrivateKey(keyBytes)
if err != nil {
return "", err
}
rsaKey := privateKey.(*rsa.PrivateKey)
hash := sha256.Sum256([]byte(stringToSign))
signature, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, hash[:])
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(signature), nil
}
在 HTTP 请求头中携带签名
将生成的签名连同时间戳和商户 ID 一起添加到 HTTP 请求头中:
Content-Type: application/json
X-TIMESTAMP: 2024-12-30T18:30:36+07:00
X-SIGNATURE: BD+qFc0/JOl1grDKSs0zMumPeswzLN/tVj/bms9AYZdVwtzZ3RQJ2gdCxtMeF/fYVTLOZZVLiKnFm0KDjoChl9Kz+6VpgFCMCQt99iUxoJxLtCvPhlCdD1rHVNfrNWKt4mLSNdQWmh//hlwP4AJqidMUKCTDMDm2ivgvQqMIFqdadITAreevKQ+VZs7ukZMzo5BgYfdhCiorAqAb/EUN1/O0pDt8Uv+xcL4QNxxu1dj10UDRbHaox4Qv1tfXbRoiisNa9xAlVkd3l19FW/Mv09jkDEKL+x5uIx3WjGY81NyVW6yxx5jEbsZbdl5KfqA6SwKpc3IOVLwR4J4Q0jaFGA==
X-PARTNER-ID: 20019
cURL 示例(沙箱代收)
curl -X POST 'https://sandbox-gateway.smilepayz.com/v2.0/transaction/pay-in' \
-H 'Content-Type: application/json' \
-H 'X-TIMESTAMP: 2024-12-30T18:30:36+07:00' \
-H 'X-SIGNATURE: BD+qFc0/JOl1grDKSs0zMumPeswzLN/tVj/bms9AYZdVwtzZ3RQJ2gdCxtMeF/fYVTLOZZVLiKnFm0KDjoChl9Kz+6VpgFCMCQt99iUxoJxLtCvPhlCdD1rHVNfrNWKt4mLSNdQWmh//hlwP4AJqidMUKCTDMDm2ivgvQqMIFqdadITAreevKQ+VZs7ukZMzo5BgYfdhCiorAqAb/EUN1/O0pDt8Uv+xcL4QNxxu1dj10UDRbHaox4Qv1tfXbRoiisNa9xAlVkd3l19FW/Mv09jkDEKL+x5uIx3WjGY81NyVW6yxx5jEbsZbdl5KfqA6SwKpc3IOVLwR4J4Q0jaFGA==' \
-H 'X-PARTNER-ID: 20019' \
-d '{"area":10,"callbackUrl":"https://docs.smilepayz.com/en/","merchant":{"merchantId":"20019"},"money":{"amount":10000,"currency":"IDR"},"orderNo":"20019c5b63c4b-e34a-4855-9b20-d4b","paymentMethod":"QRIS","purpose":"Purpose For Transaction from Java SDK"}'
沙箱基础 URL
沙箱:https://sandbox-gateway.smilepayz.com · 生产:https://gateway.smilepayz.com。详见环境 URL。
签名验证
使用下方的 RSA 公钥来验证回调,或在集成测试期间校验签名。
用于验证的 RSA 公钥
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtXfGWrq5PwNFgdV+wq1vkqrFTOL9mTBk3alhEDgCkMmPw2RJoEpu8IVDC2H6nisc8wHd5WR774sjv1fx7dovGsbYgObcg2uPOSIAcvozvVB8cyPvQkcZyGOY4wwKgIBL0WtB2RH2ByxpR9yEOANmu4PFpt8qp9qayK4GNtZzuPt9xrgUdKgqRsbYdJFe/0UnYVr9xX9c5G+8lGJ/c37moPbLS39Z8PlIu4Y1y/sRojUWLAwhzi8ysp2cZNXAX6sj2OWDEyI0IpOHCPmHzOATDfG5kwdct8/bHX2x1UyPzw/32w873yu0v1yb2Nm49U8DXmAh3HeQ8ejGIjtdq3wOVwIDAQAB
开发工具
JSON 压缩工具
| 工具 | URL | 用法 |
|---|---|---|
| JSON Formatter | https://jsonformatter.org/json-minify | 选择 "Minify JSON" 选项 |
安全最佳实践
密钥管理
- 安全存储:将私钥存储在安全、加密的存储介质中
- 密钥轮换:定期轮换 RSA 密钥对
- 访问控制:仅授权人员可访问私钥
- 备份:妥善保管私钥的安全备份
实现指南
时间戳窗口
请确保 X-TIMESTAMP 与 Smilepayz 服务器时间相差在 ±5 分钟 以内。请使用 NTP 服务器同步您的系统时钟。
请求完整性
签名生成后切勿修改请求体。若任何字段发生变化,请重新生成签名。
- 错误处理:为签名验证失败实现完善的错误处理
- 日志记录:记录签名生成尝试以供审计(切勿记录商户密钥或私钥)
常见问题与解决方案
- 签名不匹配:检查所有组成部分的顺序和格式是否正确
- 时间戳问题:确保系统时钟已与 NTP 服务器同步
- 密钥格式:检查 RSA 密钥的格式与编码(PKCS#8 私钥,Base64)
- 编码问题:确保所有字符串组成部分均使用正确的 UTF-8 编码
合规与标准
加密标准
- 算法:SHA256withRSA(SHA-256 哈希结合 RSA 签名)
- 密钥长度:建议使用至少 2048 位的 RSA 密钥
- 编码:签名输出采用 Base64 编码
- 格式:私钥采用 PKCS#8 格式
审计要求
- 签名日志:保留所有签名生成与验证的日志
- 密钥使用:跟踪密钥的使用情况与轮换计划
- 访问日志:监控对加密材料的访问
- 合规报告:生成满足监管要求的合规报告
