回调验签
大约 2 分钟
回调验签
适用于 API v2.0(RSA)。收到我们 POST 到你回调地址的通知时,更新订单前先校验 X-SIGNATURE。
不要混用请求签名
回调 不是 timestamp|merchant_secret|body。公式只有 tradeNo|X-TIMESTAMP,用商户后台的 Platform Public Key 验签。发请求签名见 请求签名。
验签公式
平台签名内容:
stringToSign = tradeNo + "|" + X-TIMESTAMP
| 部分 | 来源 |
|---|---|
tradeNo | JSON Body 字段 tradeNo |
X-TIMESTAMP | HTTP Header X-TIMESTAMP(格式同请求:yyyy-MM-dd'T'HH:mm:ssXXX) |
形状示例:
{tradeNo}|{X-TIMESTAMP}
和发请求不是同一套
- 只有 上面两段 —— 不要 minify Body,不要带
merchant_secret - 用 Platform Public Key 验签(不是商户私钥)
平台公钥
到商户后台 → Configuration Info 复制 Platform Public Key。
沙盒与生产的公钥 不同。详见 集成信息。
示例代码
点开你的语言。platformPublicKeyBase64 为 Configuration Info 里的 Platform Public Key(裸 Base64)。
Java
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/** Verify callback X-SIGNATURE. */
public class CallbackVerifier {
public static boolean verify(
String tradeNo,
String timestamp,
String signatureBase64,
String platformPublicKeyBase64) throws Exception {
// tradeNo|X-TIMESTAMP
String stringToSign = tradeNo + "|" + timestamp;
byte[] keyBytes = Base64.getDecoder().decode(platformPublicKeyBase64);
PublicKey publicKey = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(keyBytes));
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(stringToSign.getBytes(StandardCharsets.UTF_8));
return sig.verify(Base64.getDecoder().decode(signatureBase64));
}
}
PHP
<?php
/** Verify callback X-SIGNATURE. $platformPublicKeyBase64 = raw Base64 from portal. */
function verifyCallback(
string $tradeNo,
string $timestamp,
string $signatureBase64,
string $platformPublicKeyBase64
): bool {
// tradeNo|X-TIMESTAMP
$stringToSign = $tradeNo . '|' . $timestamp;
$keyPem = "-----BEGIN PUBLIC KEY-----
"
. chunk_split($platformPublicKeyBase64, 64, "
")
. "-----END PUBLIC KEY-----
";
$publicKey = openssl_pkey_get_public($keyPem);
if ($publicKey === false) {
return false;
}
return openssl_verify($stringToSign, base64_decode($signatureBase64), $publicKey, OPENSSL_ALGO_SHA256) === 1;
}
Python
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
def verify_callback(
trade_no: str,
timestamp: str,
signature_base64: str,
public_key_base64: str,
) -> bool:
"""Verify callback X-SIGNATURE. public_key_base64 = Platform Public Key from portal."""
# tradeNo|X-TIMESTAMP
string_to_sign = f"{trade_no}|{timestamp}"
public_key = serialization.load_der_public_key(base64.b64decode(public_key_base64))
try:
public_key.verify(
base64.b64decode(signature_base64),
string_to_sign.encode("utf-8"),
padding.PKCS1v15(),
hashes.SHA256(),
)
return True
except InvalidSignature:
return False
Node.js
const crypto = require("crypto");
/** Verify callback X-SIGNATURE. platformPublicKeyBase64 = Platform Public Key from portal. */
function verifyCallback(tradeNo, timestamp, signatureBase64, platformPublicKeyBase64) {
// tradeNo|X-TIMESTAMP
const stringToSign = `${tradeNo}|${timestamp}`;
const publicKey = crypto.createPublicKey({
key: Buffer.from(platformPublicKeyBase64, "base64"),
format: "der",
type: "spki",
});
const verifier = crypto.createVerify("RSA-SHA256");
verifier.update(stringToSign, "utf8");
return verifier.verify(publicKey, signatureBase64, "base64");
}
Golang
package signature
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"fmt"
)
// VerifyCallback verifies callback X-SIGNATURE. platformPublicKeyBase64 = Platform Public Key from portal.
func VerifyCallback(tradeNo, timestamp, signatureBase64, platformPublicKeyBase64 string) (bool, error) {
// tradeNo|X-TIMESTAMP
stringToSign := tradeNo + "|" + timestamp
keyBytes, err := base64.StdEncoding.DecodeString(platformPublicKeyBase64)
if err != nil {
return false, err
}
key, err := x509.ParsePKIXPublicKey(keyBytes)
if err != nil {
return false, err
}
pubKey, ok := key.(*rsa.PublicKey)
if !ok {
return false, fmt.Errorf("not an RSA public key")
}
sigBytes, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
return false, err
}
hashed := sha256.Sum256([]byte(stringToSign))
if err := rsa.VerifyPKCS1v15(pubKey, crypto.SHA256, hashed[:], sigBytes); err != nil {
return false, nil
}
return true, nil
}
验签通过后返回 SUCCESS
响应体 trim 后必须 精确等于 SUCCESS(区分大小写),不能是 JSON。完整流程见 回调通知。
快速排查
| 问题 | 先查什么 |
|---|---|
| 回调验签一直失败 | 是否误用发请求公式;是否用了当前环境的 Platform Public Key |
| 回调反复重推 | 响应是否 trim 后精确为 SUCCESS(不能是 JSON) |

