PHP处理苹果支付V2多环境公钥验证注意事项
PHP接收苹果 App Store Server Notifications V2 时,真正需要验证的不是一把固定的“Sandbox 公钥”或“Production 公钥”,而是苹果放在 JWS Header 的 x5c 证书链。正确流程是:解析 JWS 结构、使用本地可信的 Apple Root CA 验证证书链、从叶子证书取得公钥验证 ES256 签名,签名成功后再检查 environment、bundleId 和 Production 环境下的 appAppleId。
这几个步骤不能随意调换。尤其不能因为 Base64URL 解码后能看到 JSON,就直接相信其中的交易状态或环境字段。V2 的 signedPayload 本质上是 JWS,苹果官方文档明确说明它由 Header、Payload 和 Signature 三部分组成,并使用 Header 中的证书信息验证签名。
一、先弄清楚V2到底在验证什么
App Store Server Notifications V1 返回的是普通 JSON 结构,而 V2 的核心字段变成了:
{
"signedPayload": "xxxxx.yyyyy.zzzzz"
}
signedPayload 中三个以英文句点分隔的部分分别是:
- Header:描述签名算法,并包含
x5cX.509 证书链。 - Payload:通知类型、环境、应用信息以及其他业务数据。
- Signature:苹果针对前两部分生成的数字签名。
苹果文档目前描述的 JWS 证书链顺序是:叶子签名证书、Apple 中间证书、Apple Root 证书。真正用于验证这条 JWS 签名的是第一张叶子证书中的公钥,而这张证书本身又必须能够沿证书链回溯到你服务器预先信任的 Apple Root CA。
二、Sandbox和Production并不是简单换两把公钥
处理多环境时最容易出现的误区,是设计成:
if ($env === 'Sandbox') {
$publicKey = $sandboxPublicKey;
} else {
$publicKey = $productionPublicKey;
}
对于 App Store Server Notifications V2,这种思路并不准确。服务器通常不应该维护一把永久不变的“苹果交易公钥”,而是从每次 JWS Header 的 x5c 中取得叶子证书,再验证这条证书链确实归属于受信任的 Apple Root CA。
Sandbox 与 Production 真正必须分开的,是业务身份校验。苹果官方 Server Library 的验证逻辑会比较通知中的环境与验证器指定的环境,并检查 bundleId;Production 环境还要求提供并检查 appAppleId。官方实现明确要求 Production 使用 appAppleId,而 Sandbox 可以不提供。
同时,苹果文档说明 Sandbox 的通知数据中通常不存在 appAppleId,因此不能把“Production 必须存在 appAppleId”的规则机械套到 Sandbox。
| 检查项 | Sandbox | Production |
|---|---|---|
| JWS签名 | 必须验证 | 必须验证 |
| Apple证书链 | 必须验证 | 必须验证 |
| bundleId | 必须与自己的应用匹配 | 必须与自己的应用匹配 |
| environment | 必须为 Sandbox | 必须为 Production |
| appAppleId | 通常不作为必填校验项 | 应与 App Store 中的应用 Apple ID 匹配 |
三、准备服务器上的Apple根证书
验证时最重要的一条原则是:信任锚必须来自服务器自己的可信配置,而不能来自客户端提交的 JWS。
虽然 x5c 中会带上证书链,甚至包含 Apple Root 证书,但不能因为对方提交了一张名叫“Apple Root”的证书就自动信任它。攻击者完全可以构造自己的根证书、叶子证书和 JWS,如果服务器直接信任请求里的最后一张证书,整个验证过程就失去了意义。
正确做法是从 Apple PKI 获取根证书,将其随服务器配置部署,例如:
/var/www/app/config/apple-certs/
├── AppleRootCA-G2.cer
└── AppleRootCA-G3.cer
实际部署哪些根证书,应以 Apple PKI 当前公布的 Root Certificates 为准,而不是从某篇旧教程复制一组长期不更新的文件名。苹果官方 Server Library 同样要求调用方预先加载 Apple Root Certificates,作为 SignedDataVerifier 的信任根。
.cer,PHP/OpenSSL 使用前可以转换为 PEM;不要把 App Store Connect 下载的 .p8 API 私钥当成通知验签公钥。.p8 是你调用 App Store Server API 时用于生成 JWT 的开发者私钥,与苹果向你发送通知时的 JWS 验签不是一回事。
四、PHP解析JWS时先处理Base64URL
JWS 使用的是 Base64URL,而普通 base64_decode() 面对 -、_ 和省略的补位字符时不能直接照搬。建议单独封装:
<?php
function base64UrlDecode(string $value): string
{
$value = strtr($value, '-_', '+/');
$padding = strlen($value) % 4;
if ($padding !== 0) {
$value .= str_repeat('=', 4 - $padding);
}
$decoded = base64_decode($value, true);
if ($decoded === false) {
throw new RuntimeException('Base64URL 解码失败');
}
return $decoded;
}
然后检查 signedPayload 必须是三段:
$parts = explode('.', $signedPayload);
if (count($parts) !== 3) {
throw new RuntimeException('signedPayload 不是合法的 JWS');
}
[$encodedHeader, $encodedPayload, $encodedSignature] = $parts;
$header = json_decode(
base64UrlDecode($encodedHeader),
true,
512,
JSON_THROW_ON_ERROR
);
$payload = json_decode(
base64UrlDecode($encodedPayload),
true,
512,
JSON_THROW_ON_ERROR
);
这里虽然已经可以打印出 $payload,但它仍然只是“尚未验证的输入”。在 openssl_verify() 返回成功之前,不应该根据其中的交易状态发货、延长会员时间,也不应该拿里面的 environment 去选择一个可信根。
五、检查alg和x5c,不能允许算法由请求随意决定
苹果用于这类签名数据的算法是 ES256。苹果在介绍 App Store Server Library 的技术说明中也说明其 JWS Header 使用 ES256,并通过 x5c 计算和验证签名公钥。
因此PHP端不要写成下面这种形式:
// 不建议
$algorithm = $header['alg'];
openssl_verify($data, $signature, $publicKey, $algorithm);
更安全的处理是直接建立允许列表:
if (($header['alg'] ?? null) !== 'ES256') {
throw new RuntimeException('不支持的 JWS 签名算法');
}
if (
!isset($header['x5c']) ||
!is_array($header['x5c']) ||
count($header['x5c']) < 2
) {
throw new RuntimeException('JWS 缺少有效的 x5c 证书链');
}
这样即使攻击者篡改 Header,也不能让你的服务自动切换到一个更弱、甚至根本不验证签名的算法。
六、把x5c证书转换成PHP/OpenSSL可用的PEM
x5c 中每个元素是证书 DER 数据的 Base64 表示,不是 Base64URL。可以这样转换:
function x5cToPem(string $certificate): string
{
$der = base64_decode($certificate, true);
if ($der === false) {
throw new RuntimeException('x5c 证书 Base64 解码失败');
}
return "-----BEGIN CERTIFICATE-----\n"
. chunk_split(base64_encode($der), 64, "\n")
. "-----END CERTIFICATE-----\n";
}
$certificates = array_map(
static fn(string $cert): string => x5cToPem($cert),
$header['x5c']
);
$leafCertificate = $certificates[0];
这里的 $leafCertificate 才是稍后用于取得签名公钥的证书,但在使用其公钥之前,要先确认它能够连接到服务器自己的 Apple Root CA。
七、先验证证书链,再验证JWS签名
下面给出一种使用 PHP OpenSSL 扩展完成基础证书链校验的实现。假设服务器已经准备了可信 Apple Root CA 文件。
function verifyAppleCertificateChain(
array $certificates,
array $trustedRootFiles
): void {
if (count($certificates) < 2) {
throw new RuntimeException('证书链长度异常');
}
$leafCertificate = $certificates[0];
// x5c 中除叶子证书外的证书只作为 untrusted chain 使用。
// 即使其中包含 Root,也不会把它直接作为服务器的信任锚。
$untrustedChain = implode(
"\n",
array_slice($certificates, 1)
);
$tempFile = tempnam(sys_get_temp_dir(), 'apple-chain-');
if ($tempFile === false) {
throw new RuntimeException('无法创建临时证书文件');
}
try {
if (file_put_contents($tempFile, $untrustedChain) === false) {
throw new RuntimeException('无法写入临时证书链');
}
$result = openssl_x509_checkpurpose(
$leafCertificate,
X509_PURPOSE_ANY,
$trustedRootFiles,
$tempFile
);
if ($result !== true && $result !== 1) {
throw new RuntimeException('Apple x5c 证书链验证失败');
}
} finally {
@unlink($tempFile);
}
}
调用时:
$trustedRoots = [
'/var/www/app/config/apple-certs/AppleRootCA-G2.pem',
'/var/www/app/config/apple-certs/AppleRootCA-G3.pem',
];
verifyAppleCertificateChain(
$certificates,
$trustedRoots
);
$trustedRoots 的证书来自服务器本地配置;请求里的 x5c 只参与“待验证的证书链”,不能自动加入可信根列表。
八、ES256还有一个PHP常见坑:JWS签名不是OpenSSL直接要的DER格式
即使证书链完全正确,很多 PHP 实现仍然会卡在 openssl_verify()。原因在于 JWS 的 ES256 Signature 使用固定长度的原始 ECDSA R || S 格式,而 OpenSSL 通常要求 ASN.1 DER 编码的 ECDSA 签名。
ES256 使用 P-256,因此原始签名通常由两个32字节整数组成:
R:32 bytes
S:32 bytes
最终 JWS Signature:
R || S = 64 bytes
可以增加一个转换函数:
function derEncodeInteger(string $value): string
{
$value = ltrim($value, "\x00");
if ($value === '') {
$value = "\x00";
}
if ((ord($value[0]) & 0x80) !== 0) {
$value = "\x00" . $value;
}
return "\x02" . chr(strlen($value)) . $value;
}
function joseEs256SignatureToDer(string $signature): string
{
if (strlen($signature) !== 64) {
throw new RuntimeException('ES256 签名长度不是 64 字节');
}
$r = substr($signature, 0, 32);
$s = substr($signature, 32, 32);
$sequence = derEncodeInteger($r)
. derEncodeInteger($s);
return "\x30" . chr(strlen($sequence)) . $sequence;
}
之后才进入真正的 JWS 签名验证:
$publicKey = openssl_pkey_get_public($leafCertificate);
if ($publicKey === false) {
throw new RuntimeException('无法读取叶子证书公钥');
}
$rawSignature = base64UrlDecode($encodedSignature);
$derSignature = joseEs256SignatureToDer($rawSignature);
$signingInput = $encodedHeader . '.' . $encodedPayload;
$verified = openssl_verify(
$signingInput,
$derSignature,
$publicKey,
OPENSSL_ALGO_SHA256
);
if ($verified !== 1) {
throw new RuntimeException('Apple JWS 签名验证失败');
}
注意参与签名计算的是原始的Base64URL Header + 点号 + 原始Base64URL Payload,不是你解码、重新 json_encode() 后的字符串。JSON键顺序、空格或转义形式发生变化都会得到不同的字节序列。
九、签名通过以后,再判断环境
当证书链和 JWS 签名都成功以后,Payload 才进入可信范围。对大多数普通内购通知,环境和应用信息位于 data 中:
{
"notificationType": "DID_RENEW",
"data": {
"bundleId": "com.example.app",
"environment": "Production",
"appAppleId": 123456789
}
}
以上仅用于说明字段关系,不代表某一笔真实通知。
需要注意,V2 并非所有通知类型都一定使用 data。苹果当前的结构中,data、appData、summary 和 externalPurchaseToken 是互斥字段,不同通知类型可能出现不同对象。
因此,不建议只写:
$environment = $payload['data']['environment'];
可以封装成统一方法:
function extractNotificationContext(array $payload): array
{
foreach (['data', 'appData', 'summary'] as $key) {
if (isset($payload[$key]) && is_array($payload[$key])) {
return [
'environment' => $payload[$key]['environment'] ?? null,
'bundleId' => $payload[$key]['bundleId'] ?? null,
'appAppleId' => $payload[$key]['appAppleId'] ?? null,
];
}
}
if (
isset($payload['externalPurchaseToken']) &&
is_array($payload['externalPurchaseToken'])
) {
$data = $payload['externalPurchaseToken'];
$externalPurchaseId = $data['externalPurchaseId'] ?? '';
return [
'environment' => str_starts_with(
$externalPurchaseId,
'SANDBOX'
) ? 'Sandbox' : 'Production',
'bundleId' => $data['bundleId'] ?? null,
'appAppleId' => $data['appAppleId'] ?? null,
];
}
throw new RuntimeException(
'通知中没有可识别的应用环境信息'
);
}
Apple 官方 Server Library 当前处理通知时同样会针对 data、summary、externalPurchaseToken 和 appData 分别提取应用及环境信息,而不是假定所有通知都只有 data。
十、建议把Sandbox和Production配置明确分开
如果使用两个通知URL,可以在进入验证函数时就明确告诉它当前接口期望哪个环境:
$appleConfig = [
'Sandbox' => [
'bundleId' => 'com.example.app',
'appAppleId' => null,
],
'Production' => [
'bundleId' => 'com.example.app',
'appAppleId' => 123456789,
],
];
环境和应用身份校验可以写成:
function validateApplicationContext(
array $context,
string $expectedEnvironment,
array $config
): void {
if ($context['environment'] !== $expectedEnvironment) {
throw new RuntimeException(
'通知环境与当前接口环境不匹配'
);
}
if ($context['bundleId'] !== $config['bundleId']) {
throw new RuntimeException(
'bundleId 不匹配'
);
}
if ($expectedEnvironment === 'Production') {
if (
$context['appAppleId'] === null ||
(string)$context['appAppleId']
!== (string)$config['appAppleId']
) {
throw new RuntimeException(
'Production appAppleId 不匹配'
);
}
}
}
假如 Production 通知误发到了 Sandbox 接口,即使它的苹果签名完全正确,也应该被拒绝。因为“签名是真的”只能证明数据来自苹果,并不能证明它属于你当前这个应用、当前这套环境和当前这条业务流程。
十一、一个可以串起来理解的PHP验证流程
把前面的步骤组合后,主流程可以整理为:
<?php
function verifyAppleV2Notification(
string $signedPayload,
string $expectedEnvironment,
array $environmentConfig,
array $trustedRootFiles
): array {
/*
* 1. 拆分 JWS
*/
$parts = explode('.', $signedPayload);
if (count($parts) !== 3) {
throw new RuntimeException('非法 JWS 结构');
}
[$encodedHeader, $encodedPayload, $encodedSignature] = $parts;
/*
* 2. 解码 Header
*/
$header = json_decode(
base64UrlDecode($encodedHeader),
true,
512,
JSON_THROW_ON_ERROR
);
if (($header['alg'] ?? null) !== 'ES256') {
throw new RuntimeException('只允许 ES256');
}
if (
empty($header['x5c']) ||
!is_array($header['x5c'])
) {
throw new RuntimeException('缺少 x5c');
}
/*
* 3. 构建证书链
*/
$certificates = array_map(
static fn(string $cert): string => x5cToPem($cert),
$header['x5c']
);
verifyAppleCertificateChain(
$certificates,
$trustedRootFiles
);
/*
* 4. 使用叶子证书公钥验证 ES256
*/
$publicKey = openssl_pkey_get_public($certificates[0]);
if ($publicKey === false) {
throw new RuntimeException('读取苹果公钥失败');
}
$signature = joseEs256SignatureToDer(
base64UrlDecode($encodedSignature)
);
$verified = openssl_verify(
$encodedHeader . '.' . $encodedPayload,
$signature,
$publicKey,
OPENSSL_ALGO_SHA256
);
if ($verified !== 1) {
throw new RuntimeException('JWS 签名验证失败');
}
/*
* 5. 到这里才能把 Payload 当作可信数据解析
*/
$payload = json_decode(
base64UrlDecode($encodedPayload),
true,
512,
JSON_THROW_ON_ERROR
);
/*
* 6. 检查应用与环境
*/
$context = extractNotificationContext($payload);
validateApplicationContext(
$context,
$expectedEnvironment,
$environmentConfig
);
return $payload;
}
Production 接口可以这样调用:
$payload = verifyAppleV2Notification(
$request['signedPayload'],
'Production',
[
'bundleId' => 'com.example.app',
'appAppleId' => 123456789,
],
[
'/var/www/app/config/apple-certs/AppleRootCA-G2.pem',
'/var/www/app/config/apple-certs/AppleRootCA-G3.pem',
]
);
Sandbox 则明确改成:
$payload = verifyAppleV2Notification(
$request['signedPayload'],
'Sandbox',
[
'bundleId' => 'com.example.app',
'appAppleId' => null,
],
[
'/var/www/app/config/apple-certs/AppleRootCA-G2.pem',
'/var/www/app/config/apple-certs/AppleRootCA-G3.pem',
]
);
可以看到,两套环境的密码学验证代码本身并不需要复制。差别主要位于预期环境和应用身份配置。
十二、signedTransactionInfo不能再用一次简单Base64解码就结束
外层 signedPayload 通过验证以后,常见通知的 data 中还会出现:
data.signedTransactionInfo
data.signedRenewalInfo
它们本身也是 JWS。苹果文档说明 signedPayload、signedTransactionInfo 和 signedRenewalInfo 都带有可以验证的 JWS 签名。
因此,如果业务需要根据交易详情决定是否增加会员时间、确认商品ID或处理退款,推荐继续使用同样的 JWS 验证逻辑验证内部数据,而不是:
// 不建议直接这样拿交易数据做业务
$parts = explode('.', $signedTransactionInfo);
$transaction = json_decode(
base64UrlDecode($parts[1]),
true
);
更合理的结构是把前面的密码学部分抽象成通用函数:
verifyJws($signedPayload);
verifyJws($signedTransactionInfo);
verifyJws($signedRenewalInfo);
然后针对不同对象分别做业务字段验证。
十三、不要只验证公钥签名而跳过证书链
下面这种代码表面上也执行了 openssl_verify():
$leaf = x5cToPem($header['x5c'][0]);
$publicKey = openssl_pkey_get_public($leaf);
openssl_verify(
$data,
$signature,
$publicKey,
OPENSSL_ALGO_SHA256
);
但如果没有验证这张叶子证书是否最终连接到服务器自己的 Apple Root CA,它只能证明:
“这段数据由与这张证书配套的私钥签过。”
它不能证明:
“这张证书和私钥确实属于苹果。”
因此完整验证需要同时满足两部分:
- 证书链可信;
- JWS签名正确。
缺一项都不能视为完整验证。
十四、TestFlight要按Sandbox处理
另一个多环境环境中容易出现的判断错误,是认为“从 App Store Connect/TestFlight 安装的版本应该属于 Production”。对于 App Store Server Notifications,Apple 文档明确说明 TestFlight 使用 Sandbox 环境发送通知。
因此测试时如果看到:
"environment": "Sandbox"
并不一定说明通知配置错误。如果购买行为来自 TestFlight,这正是需要预期的结果。
十五、不要在签名验证之前根据environment选择信任策略
假设一个接口同时接收两个环境,有人可能会这样写:
$payload = decodeWithoutVerify($signedPayload);
if ($payload['data']['environment'] === 'Production') {
$config = $productionConfig;
} else {
$config = $sandboxConfig;
}
verify($signedPayload, $config);
问题在于,第一步读取的 environment 还没有经过任何真实性验证。
比较安全的设计是:
- 使用统一、固定的 Apple Root CA 信任策略完成证书链检查。
- 验证 JWS 签名。
- 签名成功后取得可信
environment。 - 根据环境加载对应业务配置。
- 继续验证
bundleId、appAppleId等字段。
如果使用 Sandbox 和 Production 独立 URL,则更加简单:接口本身已经知道预期环境,不需要从未验证的数据推断。
十六、证书过期和历史通知需要单独考虑
证书验证还有一个不太容易注意到的问题:你是在“现在”验证通知,还是在重新处理“很久以前”的历史通知。
Apple 官方 Server Library 提供在线检查选项。Apple 在相关说明中提到,对于刚刚收到的数据可以启用证书撤销等在线检查;如果处理几个月或几年前存储的历史数据,则需要考虑证书当前可能已经过期,而数据在签名时可能是有效的。
手写 PHP OpenSSL 验证代码时,这部分尤其要谨慎。简单调用 openssl_x509_checkpurpose() 更接近对证书“当前状态”的验证,并不能自动完整复现 Apple 官方库针对签名时间、证书状态和撤销检查的全部处理逻辑。
因此,如果项目涉及:
- 大量历史通知重新验签;
- 长期保存JWS后再次验证;
- 严格的证书撤销检查;
- 支付状态直接影响高价值资产;
最好将证书验证实现进行专门安全审计,或者让一个使用 Apple 官方 App Store Server Library 的 Java、Python、Node.js 或 Swift 服务承担验签职责,再把已经验证的数据交给 PHP。Apple 官方目前提供的 App Store Server Library 语言包括 Swift、Java、Python 和 Node.js,并未列出官方 PHP 版本。
十七、业务处理还要解决重复通知
公钥和签名验证成功,只解决“通知是真的假的”,并没有解决“同一通知是否已经处理过”。
V2 Payload 提供 notificationUUID,Apple 将其定义为通知的唯一标识,可以用来识别重复通知。
数据库可以建立唯一索引,例如:
CREATE UNIQUE INDEX uk_apple_notification_uuid
ON apple_notifications(notification_uuid);
典型处理顺序是:
验签成功
↓
验证 environment / bundleId / appAppleId
↓
检查 notificationUUID 是否处理过
↓
验证 signedTransactionInfo
↓
执行业务更新
↓
记录 notificationUUID
↓
返回成功状态
这样即使苹果因为网络或服务器响应问题重复发送同一条通知,也不会重复给用户增加权益。
十八、PHP接收接口还应注意HTTP响应
当服务器成功接收并处理 App Store Server Notification 时,需要返回成功的 HTTP 状态。Apple 当前文档说明,成功接收时可以返回 200–206;如果返回 40x 或 50x,则会被视为失败并可能触发重试机制。
一个简化的入口可以写成:
<?php
try {
$rawBody = file_get_contents('php://input');
$request = json_decode(
$rawBody,
true,
512,
JSON_THROW_ON_ERROR
);
if (empty($request['signedPayload'])) {
throw new RuntimeException('缺少 signedPayload');
}
$payload = verifyAppleV2Notification(
$request['signedPayload'],
'Production',
[
'bundleId' => 'com.example.app',
'appAppleId' => 123456789,
],
[
'/var/www/app/config/apple-certs/AppleRootCA-G2.pem',
'/var/www/app/config/apple-certs/AppleRootCA-G3.pem',
]
);
// 此处继续:
// 1. notificationUUID 幂等检查
// 2. signedTransactionInfo 验签
// 3. 根据 notificationType 更新业务状态
http_response_code(200);
} catch (Throwable $e) {
error_log(
'Apple V2 notification error: ' . $e->getMessage()
);
http_response_code(500);
}
实际项目中应根据错误类型区分“请求确定无效”和“服务器暂时失败”,而不是所有异常一律返回同一个状态码。否则一个永远无法通过验证的恶意请求也可能不断进入重试流程。
十九、几个最容易踩到的坑
1. 把x5c最后一张证书直接加入可信根
这是最危险的错误之一。可信根应该预先部署在服务器上,而不是由请求告诉服务器“请信任我提供的根证书”。
2. 只Base64URL解码,不验签
JWS Payload 本来就是可以被任何人读取的。能够得到JSON并不能证明JSON来自苹果。
3. 证书链不验证,只用叶子公钥openssl_verify
这样只能确认“签名和叶子公钥对应”,无法证明叶子证书属于Apple。
4. Sandbox强制要求appAppleId
苹果文档说明 Sandbox 数据中可能不存在该字段。Production 才应将它作为应用身份的重要校验条件。
5. TestFlight当成Production
TestFlight 的服务器通知仍然属于 Sandbox。
6. 根据未验签environment决定是否信任
environment 自己也属于被签名数据的一部分。应该先建立密码学可信性,再让它参与环境路由。
7. 外层signedPayload验证完,就直接解码内层transaction
signedTransactionInfo 和 signedRenewalInfo 本身也是JWS。涉及支付权益的字段最好继续执行完整验证。
8. 把开发者p8私钥和苹果通知公钥混在一起
你自己的 App Store Connect 私钥负责“你向苹果证明你是谁”;通知中的 x5c 和苹果证书链负责“苹果向你证明通知是苹果签的”。两者方向完全相反。
二十、应该选择单接口还是Sandbox、Production双接口
两种方案都可以实现,但如果项目允许分别配置通知URL,独立接口更容易维护:
/api/apple/notifications/sandbox
/api/apple/notifications/production
好处不是因为两边要用完全不同的公钥,而是每个入口在收到请求之前就已经知道自己的预期环境。
Sandbox入口可以明确要求:
environment === 'Sandbox'
bundleId === 'com.example.app'
Production入口明确要求:
environment === 'Production'
bundleId === 'com.example.app'
appAppleId === 123456789
如果项目必须共用一个URL,也不需要害怕,只需要遵守一个原则:
先通过统一的Apple证书链和JWS签名验证,再读取environment进行后续分流。
总结
PHP处理苹果支付V2多环境公钥验证,关键并不是为 Sandbox 和 Production 各保存一把固定公钥。V2 使用 JWS 和 x5c X.509 证书链,服务器应该从叶子证书获取当前签名公钥,同时使用自己预先部署的 Apple Root CA 验证证书链。
完整顺序可以记成:
接收 signedPayload
↓
拆分 Header / Payload / Signature
↓
检查 alg = ES256
↓
解析 x5c
↓
使用服务器本地 Apple Root CA 验证证书链
↓
取得叶子证书公钥
↓
将 ES256 JOSE 签名转换为 OpenSSL DER 格式
↓
验证 Header.Payload 的 JWS 签名
↓
签名成功后读取 environment
↓
验证 bundleId
↓
Production 再验证 appAppleId
↓
验证 signedTransactionInfo / signedRenewalInfo
↓
使用 notificationUUID 做幂等
↓
更新订单、订阅或用户权益
只做到“能Base64解码”远远不够;只做到“openssl_verify返回1”也还不完整。对于支付通知,证书链、JWS签名、环境、应用身份和业务幂等应该作为一套连续验证流程处理。这样Sandbox、TestFlight和Production同时存在时,PHP服务才能既正确识别环境,又避免把来自错误应用、错误环境或伪造证书的数据送进真实支付业务。
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













