学完这篇,你能独立走通微信支付 V3 和支付宝 RSA2 的「下单签名 → 发起支付 → 异步回调验签 → 幂等入账」全流程,知道每一处签名串到底怎么拼。
第一步:先把这四样东西备齐
不管接哪家,本质上都是同一套凭据:
- 支付宝:APPID、应用私钥(
app_private_key.pem)、支付宝公钥(alipay_public_key.pem)、签名类型 RSA2。
- 微信支付 V3:商户号 mchid、APIv3 密钥(32 位,用于回调报文解密)、商户 API 私钥与证书序列号(
serial_no)、平台证书/公钥(用于验签回调)。
注意:支付宝「应用公钥」是上传到开放平台的那把,「支付宝公钥」是平台返回给你的,两把长得像但不是一回事,验签用错必失败。
回调地址必须是公网可访问的 HTTPS。本地调试用内网穿透,别在回调里跳转、别验登录态。
第二步:支付宝下单——拼签名串
支付宝网页/APP 支付走的是「参数排序 + 私钥签名」:
$params = [
'app_id' => ALIPAY_APPID,
'method' => 'alipay.trade.page.pay',
'format' => 'JSON',
'charset' => 'utf-8',
'sign_type' => 'RSA2',
'timestamp' => date('Y-m-d H:i:s'),
'version' => '1.0',
'notify_url' => 'https://your.com/notify/alipay',
'return_url' => 'https://your.com/pay/return',
'biz_content' => json_encode([
'out_trade_no' => $orderNo,
'total_amount' => '0.01', // 字符串,单位元
'subject' => '测试订单',
'product_code' => 'FAST_INSTANT_TRADE_PAY',
], JSON_UNESCAPED_UNICODE),
];
ksort($params);
$pairs = [];
foreach ($params as $k => $v) {
if ($v !== '' && $v !== null) $pairs[] = $k . '=' . $v; // 原始值,不 urlencode
}
$signStr = implode('&', $pairs);
openssl_sign($signStr, $sign, file_get_contents('app_private_key.pem'), OPENSSL_ALGO_SHA256);
$params['sign'] = base64_encode($sign);
拿 $params 拼成跳转 URL 即可。
注意:签名串用的是未编码的原始值。很多人图省事用 urldecode(http_build_query($params)),遇到中文、%、+、& 就会和支付宝算出的串不一致,导致 sign check fail。中文务必 JSON_UNESCAPED_UNICODE,否则也会对不上。
第三步:支付宝回调——验签 + 应答
$data = $_POST;
$sign = $data['sign'];
unset($data['sign'], $data['sign_type']);
ksort($data);
$pairs = [];
foreach ($data as $k => $v) $pairs[] = $k . '=' . $v;
$ok = openssl_verify(implode('&', $pairs), base64_decode($sign),
file_get_contents('alipay_public_key.pem'), OPENSSL_ALGO_SHA256);
if ($ok === 1 && $data['trade_status'] === 'TRADE_SUCCESS') {
// 校验 app_id、out_trade_no、total_amount,再入账
}
echo 'success'; // 必须输出纯文本 success,否则支付宝持续重发
$data 里的值已经是 URL 解码后的,直接拼即可。
第四步:微信 V3 下单——构造 Authorization
微信 V3 不再用 MD5,请求头靠 SHA256-RSA 签名:
$path = '/v3/pay/transactions/native'; // 有 query 时也要带上,如 /v3/pay?x=1
$body = json_encode([
'appid' => APPID, 'mchid' => MCHID,
'description' => '测试订单', 'out_trade_no' => $orderNo,
'notify_url' => 'https://your.com/notify/wxpay',
'amount' => ['total' => 1, 'currency' => 'CNY'], // 单位:分
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
$ts = time();
$nonce = bin2hex(random_bytes(16));
$message = "POST\n{$path}\n{$ts}\n{$nonce}\n{$body}\n"; // 结尾这个换行不能少
openssl_sign($message, $sig, file_get_contents('apiclient_key.pem'), OPENSSL_ALGO_SHA256);
$auth = sprintf(
'WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"',
MCHID, $nonce, base64_encode($sig), $ts, MCH_SERIAL
);
请求头带上 Authorization: $auth、Accept: application/json、Content-Type: application/json。
注意:签名串里路径要包含 query string 但不含域名,报文必须和实际发送的 body 逐字节一致。json_encode 少加 JSON_UNESCAPED_SLASHES 会把 / 转成 \/,只要两边一致仍能过,但你自己拼 header 时如果用另一次编码就会 401。
第五步:微信回调——验签 + 解密 + 应答
回调 body 是 JSON,resource 里是密文:
$raw = file_get_contents('php://input');
$msg = json_decode($raw, true);
// 1. 平台证书验签(Wechatpay-Signature / Timestamp / Nonce / Serial)
// 验签串 = 时间戳\n随机串\n原始body\n
// 2. AES-256-GCM 解密
$plain = openssl_decrypt(
base64_decode($msg['resource']['ciphertext']),
'aes-256-gcm', API_V3_KEY, OPENSSL_RAW_DATA,
$msg['resource']['nonce'], $msg['resource']['associated_data']
);
$order = json_decode($plain, true); // out_trade_no / trade_state / amount.total
header('Content-Type: application/json');
echo json_encode(['code' => 'SUCCESS', 'message' => '成功']);
失败时要返回 4xx/5xx 并带 {"code":"FAIL","message":"原因"},微信才会重试。
第六步:入账前必须做的三件事
- 金额比对:微信
amount.total 是整数分,支付宝 total_amount 是"0.01"元字符串。统一转成整数分比较,别用 == 比浮点。
- 幂等:给
out_trade_no 建唯一索引,回调进来先 SELECT ... FOR UPDATE 查订单状态,已支付直接回 success,不重复加余额。
- 兜底:回调可能延迟或丢失,配个计划任务每 5 分钟查一次未支付订单(微信
/v3/pay/transactions/out-trade-no/{no},支付宝 alipay.trade.query),状态以主动查询为准。
注意:不要把回调当唯一依据,也不要信任回调里的金额直接写库——先查单再入账,能挡掉绝大多数伪造与错账。
小结
- 支付宝:参数 ksort → 原始值拼
k=v&k=v → 私钥 SHA256 签名 → base64;回调去掉 sign 后同样拼串,用支付宝公钥验签,应答纯文本 success。
- 微信 V3:
方法\n路径\n时间戳\n随机串\n报文\n 拼串 → 私钥签名;回调先验签再 AES-256-GCM 解密,应答 JSON。
- 金额单位不同(分 vs 元),统一转整数分比较。
- 幂等靠唯一索引 + 状态机,最终一致性靠主动查单兜底。
- 证书路径、密钥文件别放 Web 根目录,回调日志记原始 body,出问题才有得查。