导读:本期聚焦于马来西亚程序员创作的《iOS内购订阅历史查询怎么做?详解App Store Server Notifications V2与JWT验证》,敬请观看详情。订阅历史记录对不上账,是iOS内购系统最容易踩的坑之一。用户换了设备、卸载重装、退款换机,客户端拿到的交易数据往往不完整,单靠restoreCompletedTransactions很难还原完整的订阅链路。本文围绕App Store Server Notifications V2与JWT验证展开,介绍如何通过服务器通知实时接收订阅状态变更,如何使用App Store Server API查询用户完整的交易历史,以及如何解析并验证signedTransactionInfo中的JWS签名,确保数据来自苹果且未被篡改。文中还包含密钥配置、验签代码示例与常见坑点分析,适合正在搭建订阅制应用后端的开发者参考。

iOS内购订阅系统最让人头疼的问题之一,就是如何在服务器端准确掌握用户完整的订阅历史。客户端的restoreCompletedTransactions只能拉到当前Apple ID下的部分交易,App Store的沙盒环境与生产环境数据隔离,退款、家庭共享、跨设备登录等场景更会让客户端数据与真实账目产生偏差。要构建可靠的订阅体系,必须把数据源放到服务器侧,苹果为此提供了两套核心能力:App Store Server API的查询接口,以及App Store Server Notifications V2的实时推送。两者的数据都通过JWS(JSON Web Signature)格式签名传输,需要用JWT相关的算法完成验签,才能确认数据真实可信。

iOS内购订阅历史查询怎么做?详解App Store Server Notifications V2与JWT验证

一、为什么客户端方案无法还原完整订阅历史

很多团队最初的方案是在客户端完成交易后,把transaction.transactionIdentifier或receipt上传给自己的服务器,再由服务器调用苹果的verifyReceipt接口校验。这个方案在StoreKit 2时代已经不推荐使用,苹果官方明确将其标记为legacy。verifyReceipt返回的是原始收据的解析结果,格式不稳定,且无法表达订阅的完整生命周期事件,比如升级、降级、宽限期、账单重试、退款等状态变化。

更关键的是,客户端上报存在天然的不可靠性。用户完成购买后立刻杀掉应用、断网、卸载重装,都会导致交易数据没有上报到服务器。用户跨设备恢复购买时,另一台设备产生的交易你未必能感知。而退款事件只在苹果侧发生,客户端没有任何回调可以捕获。因此,完整订阅历史必须以苹果服务器为唯一可信数据源,通过服务器到服务器的通道获取。

StoreKit 2在客户端提供了Transaction.allTransaction.currentEntitlements等API,可以遍历当前Apple ID的历史交易,这对客户端体验没问题,但把它当作账务数据源是不安全的。正确的架构是:客户端只负责完成交易与展示权益,服务器通过通知与查询接口维护全量账本。

二、App Store Server Notifications V2的工作机制与配置

Notifications V2是苹果推送给你的服务器的HTTPS回调。当用户的订阅状态发生变化时,苹果会向你在App Store Connect中配置的URL发送一个POST请求,body是一个JSON,其中核心字段是signedPayload,内容是一个JWS格式的签名字符串,解码后包含notificationUUIDsubtypedata.signedTransactionInfodata.signedRenewalInfo等关键信息。

配置入口在App Store Connect的应用详情页,找到App信息中的App Store服务器通知部分,分别填写生产环境和沙盒环境的URL。苹果要求URL必须是HTTPS且使用443端口之外需显式声明,同时服务器需要能通过苹果的连通性测试。建议为每个通知做幂等处理:以notificationUUID为唯一键去重,因为苹果在收到200响应之前会按指数退避策略重试,最多重试5次,持续约半天时间。如果通知始终无法送达,苹果还提供了Get Notification History接口,可以按时间段补拉历史通知。

V2的通知类型(notificationType)覆盖了完整的订阅生命周期:SUBSCRIBED表示新订阅,DID_RENEW表示续订成功,DID_FAIL_TO_RENEW表示扣款失败进入宽限期,EXPIRED表示彻底过期,REFUND表示退款,DID_CHANGE_RENEWAL_STATUS表示用户取消或恢复了自动续订,GRACE_PERIOD_EXPIRED表示宽限期结束。每种类型配合subtype可以精确区分场景,例如SUBSCRIBEDRESUBSCRIBE表示老用户重新订阅。服务器应根据这些事件驱动订阅状态机,而不是每次都全量拉取。

三、用App Store Server API查询完整交易历史

除了被动接收通知,服务器还可以主动查询。App Store Server API提供了几个关键接口:Get Transaction History按时间正序返回某个原始交易ID下的所有交易记录,支持分页;Get All Subscription Statuses返回某个用户所有订阅的当前状态;Get Transaction Info查询单笔交易详情。这些接口弥补了通知丢失的场景,适合做每日对账任务。

调用这些接口需要先生成一个签名JWT作为Bearer Token。JWT的header中alg为ES256,kid填你在App Store Connect中创建的In-App Purchase密钥ID;payload包含iss(你的团队ID)、iatexpaud固定为appstoreconnect、bid为Bundle ID。签名私钥是以.p8文件下载的椭圆曲线私钥。以下用Python生成token的示例:

import time
import jwt  # PyJWT库

def generate_apple_token(key_path, key_id, team_id, bundle_id):
    with open(key_path, 'r') as f:
        private_key = f.read()
    headers = {"alg": "ES256", "kid": key_id}
    payload = {
        "iss": team_id,
        "iat": int(time.time()),
        "exp": int(time.time()) + 1200,  # 最长不超过20分钟
        "aud": "appstoreconnect",
        "bid": bundle_id
    }
    return jwt.encode(payload, private_key, algorithm="ES256", headers=headers)

查询交易历史时,首次请求使用原始交易ID(originalTransactionId),响应中的hasMore为true时,取revision参数继续分页拉取。每条记录同样是JWS签名的signedTransactionInfo,需要解码验证后落库。注意沙盒与生产的域名不同,沙盒用api.storekit-sandbox.itunes.apple.com,生产用api.storekit.itunes.apple.com,两者数据完全隔离,测试时要确认连的是正确的环境。

四、JWT验证:确保通知与交易数据可信

无论是通知中的signedPayload还是查询结果中的交易信息,本质上都是JWS,格式为header.payload.signature三段Base64URL编码字符串。验签分两层:第一层验证外层JWS的签名,使用苹果根证书签发的X5c证书链,证书链最顶端的根证书需要与苹果官方发布的Apple Root CA G3比对;第二层解出payload后,如果其中还有嵌套的signedTransactionInfo,需要再做一次JWS验证,两层的证书链不同,不能混用。

验签的核心是从JWS header的x5c字段提取证书链,逐级验证证书签发关系,确认叶证书的公钥能解开JWS的signature,同时校验叶证书中的O字段为Apple Inc.且OU字段为App Store。以下是一个简化但可用的Java验权思路:

// 使用java-jwt与BouncyCastle的简化示例
public DecodedJWT verifyJws(String signedPayload) throws Exception {
    DecodedJWT decoded = JWT.decode(signedPayload);
    List<String> x5c = decoded.getHeaderClaim("x5c").asList(String.class);
    // 1. 将x5c证书链逐个解析并验证签发关系
    // 2. 校验根证书指纹是否为Apple Root CA G3
    // 3. 校验叶证书的Subject: O=Apple Inc., OU=App Store, CN=Apple SubCA
    // 4. 用叶证书公钥构造ECPublicKey验签
    ECPublicKey publicKey = extractLeafPublicKey(x5c);
    Algorithm algorithm = Algorithm.ECMonitor(); // 实际使用 Algorithm.ECDSA256(null, publicKey)
    return JWT.require(algorithm).build().verify(signedPayload);
}

更省事的做法是使用苹果开源的App Store Server Library,它提供了Swift、Java、Python、Node.js和Go版本,封装了完整的证书链校验、证书吊销检查(CRL)以及payload解码,一行代码即可完成验证,比如Python版本的SignedDataVerifier。自研验签时最容易忽略的坑有三个:一是没有校验证书链的吊销状态,苹果会定期轮换中间证书;二是沙盒和生产通知的environment字段不同,验签时要把环境作为参数传入,否则会校验失败;三是不验证bundleId,理论上一个精心构造的跨应用通知可能被重放到你的接口,务必确认payload中的bundleId与你自己的应用一致。

五、落地架构与常见坑点总结

完整的订阅系统建议按事件溯源的方式设计:收到通知或查询到新交易后,先把原始JWS字符串原样存档,再解码出结构化数据写入事件表,订阅的当前状态由事件表推导或用一个状态表逐步更新。这样即使解码逻辑有bug,也可以从存档的原始数据重新回放,不会丢账。补单任务则通过Get Transaction History定期对账,比对事件表与苹果侧的差异。

几个高频踩坑点值得再次强调。第一,通知接口返回非200状态码会触发苹果重试,如果你的业务处理逻辑较慢,应该先入库返回200,再异步处理。第二,宽限期内的订阅在summary层面仍可能显示有效,需要结合gracePeriodExpiresDate字段判断,避免给已经停止付费的用户长期提供服务。第三,originalTransactionId是贯穿整个订阅生命周期的锚点,用户ID与订阅的绑定关系应建立在它之上,而不是会变化的transactionId。第四,测试时善用App Store Server API中的Extend Subscription Renewal Date接口,可以在沙盒中延长续期时间,方便模拟各种时间节点。

把客户端交易、服务器通知、主动查询三条链路整合起来,以JWS验签作为信任基础,以originalTransactionId作为关联键,你的订阅历史记录查询体系就能做到不漏账、不错账、可回溯,这也是所有订阅制iOS应用后端的标准形态。

iOS内购App Store Server Notifications V2JWT验证修改时间:2026-09-02 13:42:56

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/20260902/48966.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。