iOS ChatGPT Pro 20x 订阅链路拆解:商品标识替换与权益绑定

47 工作室
- 官网:bk.47claude.com
- 微信:
gzs-47
这篇文章整理的是一条完整的 iOS 订阅链路:客户端先向 App Store 提交商品购买请求,StoreKit 在支付成功后生成签名交易数据,应用再把交易凭证交给 RevenueCat,由 app_user_id 决定权益最终归属。
真正值得分析的不是某一个可替换字段,而是三个系统之间的信任边界:
- App Store 决定用户实际购买了哪个商品。
- Apple 签名交易凭证,证明一笔真实交易存在。
- RevenueCat 和应用后端把这笔交易映射到具体 ChatGPT 账号。
只看其中一层,很容易把“商品选择”“付款成功”和“账号获得权益”误认为同一件事。下面按网络链路把它们拆开。
一、先看全链路
一次正常的 iOS 订阅大致经历四段:
ChatGPT iOS 客户端 │ │ ① 提交商品标识、价格与订阅组信息 ▼App Store buyProduct │ │ ② 完成支付并生成 Apple 签名交易数据 ▼StoreKit Transaction / JWS │ │ ③ 客户端上传 fetch_token 与 app_user_id ▼RevenueCat receipts │ │ ④ 返回 CustomerInfo / entitlement ▼ChatGPT 后端校验并展示订阅状态Apple 的职责是确认交易,RevenueCat 的职责是管理订阅状态,ChatGPT 账号系统则负责识别用户。问题通常出现在跨系统映射,而不是密码学签名本身。
二、研究环境
设备与工具
| 项目 | 用途 |
|---|---|
| 已越狱 iPhone | 安装网络调试插件并观察 App 流量 |
| Sileo | 管理越狱软件包 |
| SSL Kill Switch 3 | 在测试设备上处理证书锁定 |
| Reqable | 抓取、断点和重写 HTTP 请求 |
| 上游网络代理 | 保证测试网络能够正常访问目标服务 |
| Windows 或 macOS 电脑 | 运行 Reqable,与手机处于同一局域网 |
代理链路
电脑端让 Reqable 监听 9000,再把它的二级代理指向本机上游代理端口。手机 Wi-Fi 的手动代理填写电脑局域网地址和 9000 端口。
iPhone │ Wi-Fi 手动代理 :9000 ▼Reqable │ 二级 HTTP 代理 127.0.0.1:7890 ▼上游网络代理 │ ▼App Store / RevenueCat / ChatGPT这个结构有两个好处:Reqable 可以完整看到应用流量,上游代理也不需要直接安装到手机。若电脑局域网地址为 192.168.1.20,手机代理服务器就填写该地址,而不是 127.0.0.1。
三、第一条链:App Store 商品标识替换
1. 定位购买请求
在 ChatGPT App 中进入:
设置 → 订阅 → 查看所有方案选择一个可见方案并进入系统确认页,但先不要完成确认。此时在 Reqable 中定位 App Store 的购买请求:
POST https://p44-buy.itunes.apple.com/WebObjects/MZBuy.woa/wa/buyProduct请求体是 Apple Plist 数据。分析时最重要的字段如下:
| 字段 | 作用 |
|---|---|
appAdamId | App Store 中的应用 ID |
bid | 应用 Bundle ID,本次样本为 com.openai.chat |
offerName | 订阅商品的内部方案名 |
salableAdamId | App Store 可售商品 ID |
mtSubscriptionAdamId | 订阅组 ID |
price | 交易价格的 milliunit 表示 |
buySubscription | 是否按订阅商品处理 |
2. 本次样本中的商品映射
下面是 2026 年 9 月流量样本中出现的映射。它们属于服务端可变数据,不应写死到长期运行的自动化程序中。
| 方案 | offerName | salableAdamId | mtSubscriptionAdamId | price |
|---|---|---|---|---|
| Go 月付 8 美元 | oai_chatgpt_go_1000_1m | 6749460546 | 无 | 8000 |
| Plus 月付 19.99 美元 | oai_chatgpt_plus_1999_1m | 6448311597 | 6749460546 | 19990 |
| Plus 年付 200 美元 | oai_chatgpt_plus_20000_1y | 6745416289 | 6749460546 | 200000 |
| Pro 5x 月付 100 美元 | oai_chatgpt_pro_10000_1m | 6759817441 | 6749460546 | 100000 |
| Pro 20x 月付 200 美元 | oai_chatgpt_pro_20000_1m | 6657954405 | 6749460546 | 200000 |
这里最容易误读的是 price。200000 不是 200000 美分,而是以千分之一货币单位表示的 200 美元。Apple 的 StoreKit 文档也明确区分了 API 中的货币单位与 JWS 载荷中的 milliunit。
3. 断点重写逻辑
以“Plus 年付请求切换到 Pro 20x 月付”这一观察样本为例,需要保持应用和订阅组上下文不变,只调整商品映射:
offerName = oai_chatgpt_plus_20000_1ysalableAdamId = 6745416289offerName = oai_chatgpt_pro_20000_1msalableAdamId = 6657954405price = 200000放行请求后,最终判断依据不是 Reqable 中“请求已发送”,而是 iOS 原生购买确认页展示的方案名称、计费周期和价格是否同时一致。三项只要有一项不匹配,就说明商品映射并未形成有效闭环。
这一步只改变 App Store 正在处理的商品选择。它不会自动决定订阅最终归属哪个 ChatGPT 账号。
四、第二条链:交易凭证与账号权益绑定
1. 为什么要单独观察回调
App Store 支付完成后,ChatGPT App 会把 Apple 交易数据提交给订阅管理层。样本中涉及两个关键端点:
https://api.revenuecat.com/v1/receiptshttps://ios.chat.openai.com/backend-api/payments/rc/ios/verify/v4-2023-04-27如果目的是研究“交易凭证如何映射到账号”,需要在付款前为这两个端点设置断点或临时阻断规则,同时保留完整请求记录。否则客户端会立即完成上传,当前登录账号会先进入正常绑定流程。
2. RevenueCat 请求中的核心字段
POST /v1/receipts 的请求通常同时携带交易证明和用户标识。下面只保留结构,所有具体令牌均使用占位符:
{ "fetch_token": "<Apple 签名交易数据>", "app_user_id": "<ChatGPT Account ID>", "product_id": "oai_chatgpt_pro_20000_1m", "price": 200, "store_country": "USA", "transaction_id": "<App Store Transaction ID>"}对应请求头的结构类似:
Authorization: Bearer <RevenueCat public SDK key>X-Client-Bundle-ID: com.openai.chatX-Platform: iOSX-StoreKit2-Enabled: true这些字段可以分成两类:
| 类型 | 字段 | 含义 |
|---|---|---|
| 交易证据 | fetch_token、transaction_id、product_id | 证明买了什么,以及交易是否真实 |
| 归属标识 | app_user_id | RevenueCat 用来定位 Customer 与权益归属的用户 ID |
RevenueCat 官方文档把 App User ID 定义为客户识别的核心标识。同一个 App User ID 可以在不同设备上恢复同一份订阅状态,匿名 ID 与自定义 ID 也可能通过登录或恢复购买形成 alias。
3. 观察账号迁移行为
在流量样本中,保持 Apple 签名数据和交易字段不变,仅替换 app_user_id,可以直接观察订阅系统是否把同一笔有效交易映射到另一名 Customer:
fetch_token = <保持原值>app_user_id = <账号 A 的 Account ID>app_user_id = <账号 B 的 Account ID>product_id = <保持原值>transaction_id = <保持原值>判断是否发生归属变化,应同时检查三处:
- RevenueCat 响应中的
subscriber.original_app_user_id与 aliases。 entitlements中的产品、购买时间和过期时间。- ChatGPT 后端校验完成后的实际账号状态。
仅看到 HTTP 200 不等于权益已经稳定落到目标账号。响应、后端校验与客户端展示三者一致,才算完整闭环。
五、成功响应应该怎么看
经过裁剪的响应结构如下:
{ "request_date": "2026-09-17T07:47:20Z", "subscriber": { "entitlements": { "chatgpt_pro": { "product_identifier": "oai_chatgpt_pro_20000_1m", "purchase_date": "2026-09-17T07:38:26Z", "expires_date": "2026-10-17T07:38:26Z" } }, "original_app_user_id": "<目标账号 ID>", "subscriptions": { "oai_chatgpt_pro_20000_1m": { "price": { "amount": 200, "currency": "USD" }, "store": "app_store", "period_type": "normal", "ownership_type": "PURCHASED", "expires_date": "2026-10-17T07:38:26Z" } } }}重点字段有四组:
entitlements:当前 Customer 获得的可用权益。product_identifier:权益对应的真实商品。original_app_user_id:RevenueCat 记录的最初用户标识。subscriptions:交易来源、价格、周期和到期时间。
若 original_app_user_id 没变化,但 aliases 中出现了新账号,说明系统可能执行了身份合并,而不是简单覆盖。后续查询任何一个 alias 都可能返回同一份 CustomerInfo,这一点在复盘时必须区分。
六、常见失败点
抓不到 HTTPS 明文
通常是证书没有正确安装、证书锁定仍然生效,或者请求走了未纳入代理的网络栈。先用普通 HTTPS 页面确认手机到 Reqable 的基础代理链路,再检查 SSL Kill Switch 3 的启用状态。
App Store 请求出现,但确认页方案没变
优先核对 offerName 与 salableAdamId 是否属于同一个商品。只改显示名称或只改价格,不能替代 App Store 对商品 ID 的识别。
支付完成后抓不到 receipts
检查阻断规则是否直接丢弃了请求。如果要分析请求体,应使用断点或保留请求副本,而不是无日志地拒绝流量。
RevenueCat 返回成功,ChatGPT 仍未显示权益
这通常说明订阅管理层与应用后端的状态尚未一致。继续检查 verify 请求、账号 ID、产品 entitlement 名称,以及服务端是否进行了交易 ID 去重或二次验证。
同一凭证再次提交失败
Apple 交易 ID、RevenueCat Customer 和应用后端都可能执行幂等或重放校验。一次有效响应不能推出交易凭证可以无限复用。
七、漏洞根因不是“价格能改”
从安全设计角度看,真正的风险不是客户端能否改动 Plist。客户端请求天然不可信,关键在于服务端是否把以下关系绑定成一个不可拆分的授权决策:
Apple 签名交易 + Bundle ID + Product ID + Subscription Group + Original Transaction ID + ChatGPT Account ID + RevenueCat Customer / aliases = 唯一可接受的权益记录只验证 Apple 签名,最多能证明“这笔交易是真的”;只相信 app_user_id,最多能知道“客户端希望把权益给谁”。只有服务端验证两者之间的既定绑定关系,才能证明“这笔真实交易就应该给这个账号”。
八、服务端加固建议
1. 服务端验证 Apple JWS
不要把客户端上传的解析结果直接当成交易事实。服务端应验证 JWS 签名、证书链、Bundle ID、环境、商品 ID、交易 ID 和签名时间,并使用 App Store Server API 做必要复核。
2. 建立不可变交易账本
以 originalTransactionId 和 transactionId 建立唯一索引,首次绑定时同时写入内部账号 ID。后续恢复购买可以走明确的账号迁移流程,但不能由客户端任意指定新归属。
3. 校验商品组合
服务端维护允许销售的 product_id、订阅组、价格与地区映射。客户端传来的 offerName、price 或 salableAdamId 只能作为提示,不能作为结算依据。
4. 正确处理 RevenueCat aliases
Webhook 对账时同时检查 original_app_user_id 与 aliases,避免一次身份合并让多个业务账号共享同一份权益。用户切换账号时,应显式定义“迁移”“恢复”与“拒绝”的策略。
5. 把成功拆成多个状态
至少区分:
purchase_createdapple_transaction_verifiedrevenuecat_customer_updatedbackend_entitlement_grantedclient_state_refreshed这样即使第三方回调延迟或失败,也不会用一个模糊的“支付成功”掩盖权益尚未完成落账的事实。
九、结论
iOS 订阅并不是一个请求,而是一组跨系统状态转换:
buyProduct决定送往 App Store 的商品选择。- StoreKit 生成带 Apple 签名的交易事实。
fetch_token把交易事实交给订阅管理层。app_user_id把订阅状态映射到具体 Customer。- ChatGPT 后端最终决定业务账号能否获得权益。
商品标识替换与账号权益迁移看似是两种操作,本质上都在检验同一件事:服务端有没有把“谁付款、买了什么、交易是否唯一、权益属于谁”绑定为一条不可被客户端拆开的信任链。
参考资料
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!














