跳至主要內容

回调验签

大约 2 分钟

回调验签

适用于 API v2.0(RSA)。收到我们 POST 到你回调地址的通知时,更新订单前先校验 X-SIGNATURE

不要混用请求签名

回调 不是 timestamp|merchant_secret|body。公式只有 tradeNo|X-TIMESTAMP,用商户后台的 Platform Public Key 验签。发请求签名见 请求签名

验签公式

平台签名内容:

stringToSign = tradeNo + "|" + X-TIMESTAMP
部分来源
tradeNoJSON Body 字段 tradeNo
X-TIMESTAMPHTTP 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)