yansongda/pay 接入 Apple 回调:App Store Server Notifications V2 验签、归属校验与幂等处理全指南

发布时间:2026/10/15 3:47:19
yansongda/pay 接入 Apple 回调:App Store Server Notifications V2 验签、归属校验与幂等处理全指南 后端金融科技【免费下载链接】pay可能是我用过的最优雅的 Alipay/WeChat/Unipay/江苏银行 的支付 SDK 扩展包了项目地址https://gitcode.com/yansongda/pay点击查看免费下载本文以 web/docs/v3/apple/callback.md 为骨架结合 docs/apple.md 技术方案与 src/Crypto/Apple/JwsVerifier.php、src/Plugin/Apple/Pay/CallbackPlugin.php 等源码展开讲解 yansongda/pay 中Pay::apple()-callback()的调用方式、JWS 验签链路、归属校验规则、幂等去重策略与确认回调写法帮助你在服务端安全接收并处理 Apple 订阅通知。在 yansongda/pay 中Apple ProviderPay::apple()的服务端能力分为三大块支付令牌验签解密payToken()本地密码学操作、App Store Server APIquery()/refund()ES256 JWT 认证以及本文的主角——App Store Server Notifications V2 回调callback()JWS 验签。读完本文你将掌握回调方法的三种参数传递方式、通知类型字段的语义、SDK 在验签之外自动执行的「归属校验」细节、以notificationUUID为核心的幂等处理方案以及如何用success()正确应答 Apple 服务器。:::warning 使用前提 Apple 实现由 AI 生成并经人工审核但由于测试资源限制尚未经过真实 Apple 环境实测详见 docs/apple.md 风险章节请在充分验证后再用于生产环境。 :::一、方法签名与最小可用示例方法名参数返回值callback无 / array / ServerRequestInterfaceCollection最简调用方式只需要两行代码Pay::config($this-config); // 是的你没有看错就是这么简单 $result Pay::apple()-callback();不传任何参数时SDK 会通过ServerRequest::fromGlobals()自动识别当前 PHP 请求取出通知 body 完成验签与解码返回一个Collection实例其中包含 App Store Server Notifications V2 通知参数notificationType、subtype、notificationUUID、signedDate、data等。从源码看callback()的完整执行路径位于 src/Provider/Apple.phpgetCallbackParams($contents)根据参数形态构造ServerRequestInterface派发CallbackReceived事件事件类见 src/Event/CallbackReceived.php携带 provider、请求内容与业务参数以[CallbackPlugin::class]为插件链调用pay()_request/_params透传给插件插件完成验签后走NoHttpRequestDirection方向直接将解码结果作为Collection返回不发任何 HTTP 请求。二、三种参数传递方式详解callback()的第一个参数支持三种形态getCallbackParams()src/Provider/Apple.php会分别处理2.1 传 null或不传$result Pay::apple()-callback();不传参时 SDK 调用ServerRequest::fromGlobals()自动从$_SERVER、php://input等全局环境构造请求对象。这种方式最省事但依赖 SAPI 对全局变量的填充。:::warning 建议仅在 php-fpm 下使用此方式swoole 常驻内存环境下全局变量不可靠请使用下方ServerRequestInterface参数传递方式。 :::2.2 ServerRequestInterface推荐 swoole 使用use Psr\Http\Message\ServerRequestInterface; $result Pay::apple()-callback($request); // $request 实现 ServerRequestInterface传入实现了 PSR-7ServerRequestInterface的请求对象如从 swoole 的$request转换而来SDK 原样接管并自动进行后续验签处理。swoole 环境下由于没有传统 PHP 全局变量这是最稳妥的传递方式。2.3 array自行解析的请求数据$result Pay::apple()-callback([signedPayload ...]);也可以自己解析请求参数后以数组传入。getCallbackParams()对数组有两种识别分支数组包含body与headers两个键时按「原始 body 请求头」构造请求适合已经把原始请求拆好的场景其他任意数组如[signedPayload ...]直接json_encode作为 body 构造请求。三、回调数据形态App Store Server Notifications V2Apple 回调的 POST body 为{signedPayload: JWS}形态对应 Apple 官方 responseBodyV2 定义。CallbackPluginsrc/Plugin/Apple/Pay/CallbackPlugin.php首先对 body 做 JSON 解析body 不是合法 JSON → 抛InvalidParamsException错误码PARAMS_APPLE_TOKEN_INVALID 9235见 src/Exception/Exception.php缺少signedPayload字段 → 抛InvalidSignExceptionSIGN_EMPTY 9501验签失败 → 抛InvalidSignExceptionSIGN_ERROR 9500。VerifySignaturePluginStub等测试桩见 tests/Stubs/Plugin/可参考其异常断言写法。3.1 常见通知类型JWS 验签解码后通过 payload 顶层notificationType字段区分通知类型。仓库文档中列出的常见类型如下notificationType含义REFUND退款REFUND_DECLINED退款被拒绝CONSUMPTION_REQUEST消费请求SUBSCRIBED订阅成功DID_CHANGE_RENEWAL_STATUS续订状态变更EXPIRED订阅过期REVOKED订阅被撤销除此之外Apple 官方还定义了续订、主动取消等更多类型与subtype细分字段完整清单与字段说明请以 Apple 官方 App Store Server Notifications 文档为准仓库文档同样注明「更多类型与字段说明请参考官方文档」。3.2 内嵌二级 JWS通知data分支内的signedTransactionInfo交易信息与signedRenewalInfo续订信息是内嵌的二级 JWS。SDK 会对这两个字段递归执行与顶层 JWS 完全相同的独立全链验签后解码见 src/Crypto/Apple/JwsVerifier.php因此最终返回的Collection中data.signedTransactionInfo是一个已验签解码后的数组含transactionId、originalTransactionId、bundleId、productId、purchaseDate等字段而非原始 JWS 字符串data.signedRenewalInfo同理。该行为在 tests/Plugin/Apple/Pay/CallbackPluginTest.php 的testCallbackSuccess用例中直接断言$result-getPayload()-get(data.signedTransactionInfo.transactionId)可直接点取。四、源码级验签流程x5c 证书链 ES256验签核心实现在JwsVerifier::verifyNode()src/Crypto/Apple/JwsVerifier.php对每个 JWS 节点顶层通知、二级交易/续订 JWS独立执行完整全链验证拆分 JWS按.拆成header.payload.signature三段任何一段为空即抛SIGN_EMPTY算法校验header 中alg必须为ES256否则抛SIGN_ERRORx5c 证书链header 必须携带x5c证书数组DER 形式 base64叶子证书在首位Apple Root CA - G3 在末位。SDK 校验「链长 ≥ 2 且末位证书 DER 与配置根证书一致」不硬编码恰好 3 张——Apple 未来追加交叉签名证书时依然兼容链验证openssl_x509_verify逐级验证 leaf → intermediate → rootOID 检查叶子证书必须包含1.2.840.113635.100.6.11.1中间证书必须包含1.2.840.113635.100.6.2.1与支付令牌 PKCS#7 链的 OID 不同签名验证JWS 签名为 rawr ∥ s格式先经rawToDerSignature转成 DER 再交给openssl_verifyPHP openssl 对 EC 密钥只接受 DER 签名以OPENSSL_ALGO_SHA256验证payload 解码验签通过后 base64url 解码出通知数据。验签失败统一抛InvalidSignException9500/9501与仓库所有 Provider 的验签异常惯例一致异常类见 src/Exception/InvalidSignException.php。测试侧通过 tests/Support/Apple/AppleJwsFactory.php 使用 tests/Cert/apple/ 下的 fixture 证书链jws-leaf / jws-intermediate / token-root运行时构造 JWS并覆盖了篡改 payload、篡改二级 JWS、二级 JWS 缺 x5c 等反路径用例。五、归属校验防「真实签名但错误归属」的伪造通知验签通过只代表「确实是 Apple 签发的」但还可能是别的 App / 别的环境的通知例如攻击者用自己 App 的真实签名通知你的服务端。为此SDK 对齐 Apple 官方库app-store-server-library在 assertOwnership() 中自动执行归属校验bundleId 校验若配置了bundle_id则通知内的bundleId必须与之一致否则抛验签异常environment 校验通知内的environment必须与mode匹配PayMode::SANDBOX沙箱→ 必须为SandboxPayMode::NORMAL正式→ 必须为ProductionPayMode::SERVICE服务商→ 同样对应Production。因此TestFlightXcode与本地 StoreKit 测试LocalTesting环境的通知会被拒绝——这两类环境不属于你配置的沙箱/生产语义。关于校验的边界仓库文档明确了两点未配置 App Store Server API 四件套issuer_id/bundle_id/api_key_id/api_private_key的租户会跳过bundleId校验没有配置无从比对但environment校验始终执行数据形态优先取data/summary/externalPurchaseToken/appData首个存在分支externalPurchaseToken分支无environment字段SDK 按官方库行为从externalPurchaseId的SANDBOX前缀推断环境。对应的mode枚举定义见 src/Enum/PayMode.phpNORMAL 0、SANDBOX 1、SERVICE 2配置中推荐直接写枚举如PayMode::SANDBOX。六、幂等处理以 notificationUUID 为主键Apple 通知可能重复投递网络重试、多端点投递等因此业务层必须以notificationUUID为主键做幂等去重。SDK 的行为是在返回的Collection中原样返回notificationUUID字段顶层不做任何持久化——去重完全由业务方负责。一个典型的幂等处理模式$result Pay::apple()-callback(); $uuid $result-get(notificationUUID); // 伪代码先查库已处理过则直接返回 if (OrderNotify::where(notification_uuid, $uuid)-exists()) { return Pay::apple()-success(); } // 首次处理根据 notificationType 分发业务逻辑 switch ($result-get(notificationType)) { case SUBSCRIBED: // 开通订阅权益 break; case EXPIRED: // 关闭订阅权益 break; case REFUND: // 处理退款 break; } OrderNotify::create([notification_uuid $uuid, raw $result-all()]);将notificationUUID作为唯一索引落库既是幂等键也方便事后对账与回调重放排查。七、回调所需配置项callback()的验签依赖 AppleConfig 中的证书与模式配置配置类见 src/Config/AppleConfig.php完整字段说明见 docs/apple.md 3.2 节。与回调强相关的字段如下字段类型必填说明merchant_idstring是商户 IDApple Developer 后台生成payment_processing_certstring是支付处理证书 PEM传路径或 PKCS#8 内容payment_processing_cert_passphrase?string否私钥口令apple_root_ca?string否覆盖内置信任锚 src/Certificate/AppleRootCA-G3.pem路径或 PEM 内容apple_intermediate_ca?string否覆盖内置中间 CA src/Certificate/AppleAAICAG3.pemissuer_id / bundle_id / api_key_id / api_private_keystring仅 API 功能App Store Server API 四件套参与bundleId归属校验notify_url?string否文档用SDK 不主动上报modePayMode是PayMode::NORMAL/PayMode::SANDBOX/PayMode::SERVICE完整配置示例可参考 tests/TestCase.phpapple [ default [ merchant_id merchant.com.yansongda.pay, payment_processing_cert __DIR__./Cert/apple/merchant.pem, apple_root_ca __DIR__./Cert/apple/token-root.crt, issuer_id 69a6de87-test-issuer-id, bundle_id com.yansongda.pay.test, api_key_id X5D4K9J2Q1, api_private_key __DIR__./Cert/apple/api.key, mode PayMode::SANDBOX, ], ],配置校验规则validateRequired()见 src/Config/AppleConfig.phpmerchant_id与payment_processing_cert必填否则抛CONFIG_APPLE_INVALID 9413API 四字段issuer_id/bundle_id/api_key_id/api_private_key要么全部配置、要么全部不配置只配置其中一部分会抛CONFIG_APPLE_INVALID仅做验签解密的租户可以完全不配 API 四件套此时跳过bundleId校验但environment校验仍生效。八、确认回调固定 JSON 应答Apple 要求服务端在收到通知后返回确认应答否则会持续重试投递。SDK 提供success()方法src/Provider/Apple.php返回 HTTP 200 与固定 JSONPay::config($this-config); return Pay::apple()-success();应答内容为固定 JSON{result:success}Content-Type 为application/json。success()无任何配置参数见 web/docs/v3/apple/response.md。无论业务是否重复投递幂等命中都应返回success()从而告知 Apple 已成功接收。九、测试验证与反路径保障仓库为回调链路提供了完整的正向与反路径测试见 tests/Plugin/Apple/Pay/CallbackPluginTest.php正向testCallbackSuccess构造带SUBSCRIBED通知类型与内嵌交易 JWS 的完整通知断言notificationType、subtype、notificationUUID及二级data.signedTransactionInfo.transactionId均正确解码Provider 集成testProviderCallbackLinks直接调用Pay::apple()-callback($request)验证端到端链路反路径body 非 JSONPARAMS_APPLE_TOKEN_INVALID、缺signedPayloadSIGN_EMPTY、篡改顶层 payloadSIGN_ERROR、篡改二级 JWSSIGN_ERROR、二级 JWS 缺 x5c 一律拒绝、_request类型非法PARAMS_CALLBACK_REQUEST_INVALID 9221。这些用例同时印证了「每个 JWS 均独立全链验证」「二级 JWS 必须自带 x5c」等对齐官方库的行为。十、注意事项与生产建议充分验证后再上线Apple 实现未经真实环境端到端实测无测试资源建议先在沙盒环境配合 StoreKit 测试完整走一遍订阅/退款通知链路环境匹配确保配置的mode与通知environment对应沙盒用PayMode::SANDBOX生产用PayMode::NORMAL/PayMode::SERVICETestFlight 与本地 StoreKit 测试通知会被拒绝属预期行为幂等优先所有业务副作用开通/关闭权益、退款入账必须以notificationUUID为主键去重后再执行内嵌字段已解码返回的Collection中data.signedTransactionInfo/data.signedRenewalInfo已是验签解码后的数组可直接用于业务判断无需再次验签异常处理捕获InvalidSignException9500/9501记录告警——验签失败通常意味着伪造或配置错误不要当作正常业务处理。如需在回调之外自行组合 Apple 插件如QueryHistoryPlugin查询交易历史、RefundPlugin查询退款历史等可参考 web/docs/v3/apple/all.md 中基于Pay::apple()-pay($allPlugins, $params)的自由插件链用法。赞分享后端金融科技【免费下载链接】pay可能是我用过的最优雅的 Alipay/WeChat/Unipay/江苏银行 的支付 SDK 扩展包了项目地址https://gitcode.com/yansongda/pay点击查看免费下载相关推荐yansongda/pay 微信回调处理全指南callback() 验签解密与实战用法yansongda/pay 微信回调处理全指南callback 验签解密与实战用法 本篇技术指南聚焦 yansongda/pay 微信Wechat支付回调后端金融科技yansongda/pay 微信支付 v2 接入全指南配置、支付、退款、回调验签与服务商模式yansongda/pay 微信支付 v2 接入全指南配置、支付、退款、回调验签与服务商模式 本篇技术指南以 微信概述 https://link.gitcod后端金融科技基于 yansongda/pay 的 PayPal V2 支付快速接入指南从下单、捕获到回调验签基于 yansongda/pay 的 PayPal V2 支付快速接入指南从下单、捕获到回调验签 本文以仓库 web/docs/v3/quick start/金融科技后端上一篇react-admin 记录锁实时查询指南useGetLockLive Hook 从入门到实战下一篇react-admin useStore 深度指南读写全局持久化 Store 的 useState 式 Hook创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询