支付宝异步回调SDK是否有PHP或Golang验签示例?
结论
支付宝异步通知验签不依赖 Java。PHP 可以调用支付宝开放平台 SDK 的验签方法,Golang 也可以通过标准库完成 RSA2 验签。
文档页面使用 Java 演示接收流程,并不意味着异步通知只能用 Java 处理。支付宝会向商户配置的回调地址发送 HTTP POST 表单参数。服务端按照支付宝的签名规则生成待验签字符串,再用支付宝公钥验证 sign。
支付宝 SDK 的类名和调用方式可能因 SDK 版本以及普通公钥模式、证书模式的不同而有所变化。以下示例使用常见的 RSA2 和支付宝公钥模式。
PHP 验签示例
项目已经引入支付宝 PHP SDK 时,可以使用 SDK 提供的 AlipaySignature:
<?php
require_once __DIR__ . '/vendor/autoload.php';
$params = $_POST;
$alipayPublicKey = <<<KEY
-----BEGIN PUBLIC KEY-----
你的支付宝公钥内容
-----END PUBLIC KEY-----
KEY;
$signature = new AlipaySignature();
$verified = $signature->rsaCheckV1(
$params,
$alipayPublicKey,
'RSA2'
);
if (!$verified) {
http_response_code(400);
echo 'failure';
exit;
}
// 验签通过后仍需校验业务参数
$outTradeNo = $params['out_trade_no'] ?? '';
$tradeNo = $params['trade_no'] ?? '';
$tradeStatus = $params['trade_status'] ?? '';
$totalAmount = $params['total_amount'] ?? '';
$appId = $params['app_id'] ?? '';
if ($tradeStatus === 'TRADE_SUCCESS' || $tradeStatus === 'TRADE_FINISHED') {
// 1. 根据 out_trade_no 查询本地订单
// 2. 校验 app_id、total_amount、seller_id 等关键字段
// 3. 幂等更新订单状态
// 4. 记录 trade_no,避免重复处理
}
echo 'success';
如果当前 SDK 中没有 AlipaySignature,请以实际安装版本的说明为准。部分版本可能通过 AopClient 或其他封装方法完成异步通知验签,底层验签规则相同。
Golang 验签示例
Golang 可以直接使用标准库完成 RSA2 验签:
package main
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"io"
"net/http"
"net/url"
"sort"
"strings"
)
func buildSignContent(values url.Values) string {
keys := make([]string, 0, len(values))
for key := range values {
if key == "sign" || key == "sign_type" {
continue
}
if values.Get(key) == "" {
continue
}
keys = append(keys, key)
}
sort.Strings(keys)
parts := make([]string, 0, len(keys))
for _, key := range keys {
parts = append(parts, key+"="+values.Get(key))
}
return strings.Join(parts, "&")
}
func parseRSAPublicKey(publicKeyPEM string) (*rsa.PublicKey, error) {
block, _ := pem.Decode([]byte(publicKeyPEM))
if block == nil {
return nil, errors.New("invalid public key PEM")
}
if publicKey, err := x509.ParsePKIXPublicKey(block.Bytes); err == nil {
rsaPublicKey, ok := publicKey.(*rsa.PublicKey)
if !ok {
return nil, errors.New("public key is not RSA")
}
return rsaPublicKey, nil
}
if publicKey, err := x509.ParsePKCS1PublicKey(block.Bytes); err == nil {
return publicKey, nil
}
return nil, errors.New("unsupported RSA public key format")
}
func verifyRSA2(values url.Values, publicKeyPEM string) error {
signature, err := base64.StdEncoding.DecodeString(values.Get("sign"))
if err != nil {
return err
}
publicKey, err := parseRSAPublicKey(publicKeyPEM)
if err != nil {
return err
}
content := buildSignContent(values)
digest := sha256.Sum256([]byte(content))
return rsa.VerifyPKCS1v15(
publicKey,
crypto.SHA256,
digest[:],
signature,
)
}
func notifyHandler(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, "failure", http.StatusBadRequest)
return
}
const alipayPublicKey = `-----BEGIN PUBLIC KEY-----
你的支付宝公钥内容
-----END PUBLIC KEY-----`
if err := verifyRSA2(r.PostForm, alipayPublicKey); err != nil {
http.Error(w, "failure", http.StatusBadRequest)
return
}
outTradeNo := r.PostForm.Get("out_trade_no")
tradeNo := r.PostForm.Get("trade_no")
tradeStatus := r.PostForm.Get("trade_status")
totalAmount := r.PostForm.Get("total_amount")
appID := r.PostForm.Get("app_id")
_, _, _, _ = outTradeNo, tradeNo, totalAmount, appID
if tradeStatus == "TRADE_SUCCESS" || tradeStatus == "TRADE_FINISHED" {
// 查询并校验本地订单,然后执行幂等更新。
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
_, _ = io.WriteString(w, "success")
}
生成待验签字符串时,按以下规则处理:
- 排除
sign和sign_type。 - 排除值为空的参数。
- 按参数名进行字典序排序。
- 按
key=value格式使用&连接。 - 使用支付宝公钥验证
sign。 RSA2对应SHA256withRSA,也就是 SHA-256 加 RSA PKCS#1 v1.5 签名。
接入步骤
1. 接收原始 POST 表单
支付宝异步通知通常使用 application/x-www-form-urlencoded。应从框架解析后的表单参数中读取数据,不要只按 JSON 反序列化。
2. 使用支付宝公钥验签
验签需要使用支付宝公钥,而不是应用公钥或应用私钥。应用私钥用于为商户请求签名,支付宝公钥用于验证支付宝发送的数据。
3. 校验业务字段
验签通过只能证明通知由对应私钥签名,不能代替业务校验。至少需要核对:
app_id是否属于当前应用;out_trade_no是否存在;total_amount是否与订单金额一致;seller_id是否为预期收款账号;- 当前订单状态是否允许变更;
trade_status是否为业务需要处理的状态。
4. 幂等处理通知
支付宝可能重复发送同一笔异步通知。不能仅凭收到一次回调就执行处理,应结合 out_trade_no、trade_no 和本地订单状态实现幂等。
5. 返回指定响应
全部处理成功后返回以下纯文本:
success
验签失败或业务处理失败时,不要返回 success,否则支付宝可能认为通知已经处理完成。具体的重试机制和响应要求,请以当前支付宝开放平台文档为准。
常见问题
不要对参数再次 URL 解码
Web 框架通常已经完成表单解码。再次执行 URL Decode 可能改变 +、% 或中文内容,导致生成的待验签字符串与支付宝签名时使用的内容不一致。
不要修改参数后再验签
验签必须使用回调中收到的参数。类型转换、金额格式化、去除空格或重新编码都可能导致验签失败。例如,不能先把 100.00 转成数字 100,再用它参与验签。
多值参数要结合实际 SDK 处理
上面的 Golang 示例通过 values.Get(key) 获取每个字段的第一个值,适用于支付宝常见的单值回调字段。如果接入场景中存在同名多值参数,应按照对应产品的签名规范处理,不要自行猜测拼接方式。
公钥模式与证书模式不能混用
如果应用使用证书模式,需要根据通知中的证书序列号选择并验证相应的支付宝公钥证书,不能直接套用普通支付宝公钥模式的配置。证书解析、序列号校验和证书链处理,应优先使用与当前接入模式匹配的官方 SDK。
日志中避免泄露敏感信息
日志可以记录验签结果、订单号、支付宝交易号和异常原因,但不应写入应用私钥、完整证书材料或其他敏感配置。
备注:内容仅供参考。
