不能直接用$_post或php://input,因$_post仅解析x-www-form-urlencoded,对json/xml无效;而php://input只能读一次,若框架提前访问$_post会清空流,导致验签时拿不到原始报文。

为什么不能直接用 $_POST 或 file_get_contents('php://input') 解析回调数据
微信/支付宝的支付回调是通过 POST 发送的原始 JSON 或表单数据,但 PHP 的 $_POST 会自动解析 application/x-www-form-urlencoded,而对 application/json 完全无效;反过来,file_get_contents('php://input') 能读到原始体,但若已触发过 $_POST 或 $_REQUEST 访问(比如框架自动解析),PHP 会清空该流,导致二次读取为空。实际中常见「签名验不过」,其实根本没拿到原始报文。
- 微信回调默认是
application/xml,支付宝 V2 是application/json,V3 则强制使用application/json+Authorization头 - 必须在任何超全局数组访问前,用
file_get_contents('php://input')一次性读取原始数据 - 若用 Laravel、ThinkPHP 等框架,需确认其是否已提前解析并清空了输入流;Laravel 中应使用
$request->getContent(),而非$request->all()
微信支付回调签名验证:注意字段排序和空值过滤
微信的签名算法是「对所有非空参数按字段名 ASCII 升序拼接 key=value&,末尾加商户密钥 key,再 MD5」。最容易出错的是字段来源和过滤逻辑:
- 签名字段必须来自原始 XML 解析后的关联数组,不是
$_GET或 URL 参数 - 微信回调里
sign字段本身不参与签名计算,必须先从数组中 unset 掉 - 所有值需原样使用(不 urldecode、不 trim),但空字符串
''和null值必须剔除(微信文档写的是「非空」,实测空格字符串' '也算有效,不能删) - 商户密钥
key是你在微信商户平台设置的「API 密钥」,不是 APIv3 的证书密钥
$xml = file_get_contents('php://input');
$data = json_decode(json_encode(simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA)), true);
unset($data['sign']);
ksort($data);
$string = '';
foreach ($data as $k => $v) {
if ($v !== '' && $v !== null) {
$string .= $k . '=' . $v . '&';
}
}
$string .= 'key=' . $apiKey;
$calculated = strtoupper(md5($string));
if ($calculated !== $data['sign']) {
// 验签失败
}
支付宝回调验签:别漏掉 charset 和 sign_type
支付宝回调是标准表单格式(application/x-www-form-urlencoded),但签名计算时必须使用原始未 decode 的 value 字符串,且受 charset 和 sign_type 影响:
围绕关键发现、作用机制、临床相关性及研究局限性展开讨论。适用于撰写或优化任何生物医学论文的“讨论(Discussion)”部分——包括结果解读、与既往文献关联、阐释意外发现、界定研究局限性,以及撰写结论。当用户输入以下任一指令时也会自动触发该功能: - “write my discussion” - “help me discuss my findings” - “how do I compare to prior studies” - “write the limitations par
- 先从原始 POST 流中提取完整 query string(不要用
$_POST),再用parse_str($raw, $params)解析 -
sign、sign_type、charset这三个字段不参与签名,必须提前 unset - 若
charset=utf-8,所有参与签名的 value 必须按 UTF-8 编码后再拼接(PHP 默认就是,但如果你做了 mb_convert_encoding 就可能翻车) -
sign_type通常是RSA2,对应验签函数是openssl_verify(..., OPENSSL_ALGO_SHA256);若为RSA,则用OPENSSL_ALGO_SHA1
$raw = file_get_contents('php://input');
parse_str($raw, $params);
$sign = $params['sign'] ?? '';
$signType = $params['sign_type'] ?? 'RSA2';
$charset = $params['charset'] ?? 'UTF-8';
unset($params['sign'], $params['sign_type'], $params['charset']);
<p>// 按参数名升序拼接(键值对间无空格,=前后无空格)
ksort($params);
$string = http_build_query($params, '', '&', PHP_QUERY_RFC3986);</p><p>// 注意:http_build_query 默认 urlencode,支付宝要求原始 value,所以得手动拼
$string = '';
foreach ($params as $k => $v) {
$string .= $k . '=' . $v . '&';
}
$string = rtrim($string, '&');</p><p>$pubKey = "-----BEGIN PUBLIC KEY-----\n" . wordwrap($alipayPublicKey, 64, "\n", true) . "\n-----END PUBLIC KEY-----";
$rsa = openssl_pkey_get_public($pubKey);
$result = openssl_verify($string, base64_decode($sign), $rsa, $signType === 'RSA2' ? OPENSSL_ALGO_SHA256 : OPENSSL_ALGO_SHA1);
openssl_free_key($rsa);
if ($result !== 1) {
// 验签失败
}</p>
验签失败时怎么快速定位问题
签名失败几乎从不因为算法写错,而是原始数据被污染或字段理解偏差:
- 用
error_log('RAW: ' . file_get_contents('php://input'), 3, '/tmp/alipay.log')把原始请求体打出来,和文档示例逐字符比对(注意换行、BOM、空格) - 检查是否误把
notify_id(支付宝)或nonce_str(微信)当签名字段参与了计算 - 微信回调可能含 CDATA,
simplexml_load_string默认丢内容,加LIBXML_NOCDATA标志 - 支付宝的
subject或body可能含中文、emoji,确保整个流程没做额外转码(如iconv、mb_convert_encoding)
最常被忽略的一点:验签通过后,必须立刻返回成功响应(HTTP 200 + plain text 'success'),且不能有任何额外输出(包括空格、BOM、debug echo)——否则微信/支付宝会持续重发。










