苹果支付接入涉及客户端和服务端两部分,客户端负责拉起支付并拿到收据(Receipt),服务端则要完成收据的验证和订单状态的落库。验证环节本质上是一次与App Store服务器的加密通信,理解其中的编码、传输和验签逻辑,是用PHP实现苹果支付的关键。本文将围绕收据验证流程、Apple Pay的签名数据处理以及常见错误排查三个层面,把整个加密验证链路讲清楚。

一、苹果内购收据验证的完整流程与PHP实现
苹果内购(In-App Purchase)的验证核心是收据数据。客户端完成支付后,iOS系统会生成一段经过苹果签名的收据数据,格式为PKCS#7容器,里面包含了交易ID、产品ID、购买时间等信息。这段数据以Base64编码的形式传给服务端,服务端再通过HTTPS请求苹果的验证接口完成校验。整个过程中PHP并不需要自己解密收据,而是把验证工作交给苹果服务器,这也是最安全的做法。
苹果提供了两个验证地址:沙盒环境使用 https://sandbox.itunes.apple.com/verifyReceipt,正式环境使用 https://buy.itunes.apple.com/verifyReceipt。一个常见的最佳实践是先用正式环境验证,如果返回状态码21007(表示收据是沙盒收据),再切换到沙盒环境重试一次。这样在测试和上线阶段都不需要修改代码。
下面是一个完整的PHP验证实现,使用cURL发送请求:
<?php
function verifyReceipt($receiptData, $isSandbox = false)
{
// 正式环境和沙盒环境的验证地址
$productionUrl = 'https://buy.itunes.apple.com/verifyReceipt';
$sandboxUrl = 'https://sandbox.itunes.apple.com/verifyReceipt';
// 先请求正式环境
$response = sendRequest($productionUrl, $receiptData);
if ($response['status'] === 21007) {
// 21007表示这是沙盒收据,切换到沙盒环境重试
$response = sendRequest($sandboxUrl, $receiptData);
}
return $response;
}
function sendRequest($url, $receiptData)
{
$payload = json_encode([
'receipt-data' => $receiptData, // Base64编码的收据
'exclude-old-transactions' => true
]);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_TIMEOUT => 30,
CURLOPT_SSL_VERIFYPEER => true, // 生产环境务必开启证书校验
]);
$result = curl_exec($ch);
curl_close($ch);
return json_decode($result, true);
}
// 客户端上传的收据通常是Base64字符串
$receipt = $_POST['receipt'] ?? '';
$result = verifyReceipt($receipt);
if ($result['status'] === 0) {
echo '验证成功,交易ID: ' . $result['receipt']['in_app'][0]['transaction_id'];
} else {
echo '验证失败,状态码: ' . $result['status'];
}
验证成功时返回的status为0,此时可以从receipt.in_app数组中取出交易详情。务必校验product_id是否与订单匹配、transaction_id是否重复(防止同一笔交易多次发货),以及bundle_id是否与自己的App一致。这三个字段任何一个被忽略,都可能被恶意构造的请求利用。
二、Apple Pay支付令牌的解密与签名验证
Apple Pay和内购是两套不同的体系。Apple Pay是银行卡支付,客户端返回的是一个经过加密的Payment Token,包含动态支付数据、签名信息以及商家的商户证书。服务端需要使用商户私钥对token中的加密数据完成解密,这一步才真正涉及加密操作。
Apple Pay的Payment Token采用了三层加密结构:首先是用商家公钥(ECDH,曲线为prime256v1)做的对称密钥协商,其次是用这个协商出来的密钥通过AES-128-GCM解密支付数据,最后还要验证苹果CA签发的签名链。PHP 7.1以上内置的OpenSSL扩展可以完整支撑这套流程,核心步骤如下:
<?php
// Payment Token结构包含 header、signature、version、data 四部分
// 1. 用商户私钥和header中的ephemeralPublicKey做ECDH密钥协商
function deriveKey($merchantPrivateKeyPem, $ephemeralPublicKeyDer)
{
$privateKey = openssl_pkey_get_private($merchantPrivateKeyPem);
// 公钥需要从DER转成PEM格式才能被openssl识别
$pubPem = "-----BEGIN PUBLIC KEY-----\n"
. chunk_split(base64_encode($ephemeralPublicKeyDer), 64)
. "-----END PUBLIC KEY-----\n";
$publicKey = openssl_pkey_get_public($pubPem);
$sharedSecret = openssl_dh_compute_key($publicKey, $privateKey);
// 2. 派生对称密钥:SHA256(苹果指纹 || sharedSecret || 商家ID)
$merchantId = hex2bin('你的商家ID十六进制串');
$symmetricKey = hash('sha256', hex2bin('636f6d2e6170706c652e7061796d656e74') . $sharedSecret . $merchantId, true);
return $symmetricKey;
}
// 3. 用AES-128-GCM解密data字段
function decryptData($symmetricKey, $cipherText, $tag)
{
$iv = substr($cipherText, 0, 16); // 前16字节是IV
$data = substr($cipherText, 16, -16); // 中间是密文
// GCM模式解密,PHP 7.1以上支持
$plain = openssl_decrypt($data, 'aes-128-gcm', $symmetricKey, OPENSSL_RAW_DATA, $iv, $tag);
return json_decode($plain, true); // 得到银行卡号、有效期等支付信息
}
解密成功后会得到JSON格式的支付数据,包含设备账号后四位、金额、货币类型等。实际业务中更推荐的做法是不在本地解密,而是直接把token转发给支付网关(如Stripe、Adyen)处理,它们已经封装好了整套解密和验签逻辑。只有在自建支付通道的场景下才需要自己实现上述代码。
三、常见错误码排查与安全加固建议
验证过程中最常遇到的错误码有三个:21002表示收据数据格式错误,通常是Base64编码在传输过程中被破坏,比如客户端把加号(+)变成了空格,或者URL编码处理不当,服务端收到后应先检查并修复Base64字符串;21007表示沙盒收据发到了正式环境,按前文所述做二次验证即可;21008则相反,是正式收据发到了沙盒环境。
安全方面有几个必须落实的点。第一,验证请求要开启CURLOPT_SSL_VERIFYPEER,避免被中间人劫持伪造验证结果;第二,苹果服务器偶尔会返回超时,务必实现重试机制,但要设置幂等锁,防止重试导致重复发货;第三,订单与交易的绑定关系要落库,transaction_id建唯一索引,同一交易只处理一次。
另外,从安全角度考虑,不建议把验证逻辑完全信任客户端的回调结果。正确的顺序是:客户端支付成功后上报收据,服务端同步或异步完成苹果验证,验证通过后再更新订单状态并下发权益。整个链路中客户端只负责传输收据,任何资金相关的判断都以服务端与苹果服务器的通信结果为准。只要遵循这个原则,配合上文的双环境验证和幂等处理,就能用PHP搭建出一套稳定可靠的苹果支付验证系统。
苹果支付PHPApple Pay验证内购凭据修改时间:2026-09-05 23:42:49