APP内购为什么不能只相信客户端?Play Billing服务端验单与加固安全指南
APP客户端显示“购买成功”不能直接发放会员、道具、订阅或模型额度。正确做法是把商店交易凭证提交给业务后端,由后端核验交易状态、商品、账号映射、重复处理、退款和订阅替换,再决定是否写入权益台账。御盾APP加固负责保护客户端交易入口、凭证提交链和关键业务调用,提高补丁、重打包、Hook及仿冒客户端的成本;它不替代Google Play或Apple商店验单,也不替代客户后端的最终裁决。
摘要
内购安全不是把一个本地判断加固起来,而是把“商店交易事实—服务端验证—账号权益台账—客户端展示”分成不同责任层。Android使用Play Billing取得purchaseToken后,应由后端调用Google Play Developer API核验;iOS使用StoreKit交易和JWS证明,并由App Store Server API同步订阅、退款和撤销状态。客户端可以提供体验反馈,却不应拥有最终的发放权。
读者对象
本文适合游戏、会员、工具、AI应用、直播、教育和数字内容团队的研发、安全、产品、测试与采购人员,也适合正在评估APP加固、应用真实性或内购防盗刷PoC的企业。御盾由西安守界御盾信息安全技术有限公司提供,产品范围和接入方式以御盾APP加固产品页为准。
核心结论
- 客户端的PurchaseState、UI文案或本地数据库都不是最终权益事实。
- purchaseToken必须进入后端验证和幂等处理;PENDING不能提前发放,退款和撤销必须能回收权益。
- 订阅替换要处理linkedPurchaseToken,避免新旧订阅同时保留。
- Play Integrity、App Attest与御盾提供的是应用或运行环境信号和客户端保护,不是永久封禁依据。
- 发布前应以原始Release、基础保护包、目标保护包和最终商店包进行对照验收。
一、客户端购买成功为什么不能直接发会员
客户端运行在用户可控制的环境中。攻击者可以修改购买回调、替换本地状态、拦截权益刷新请求,或制作只保留“购买成功”分支的仿冒包。即使应用使用了官方Billing SDK,也不能因此把客户端布尔值当作财务事实。下面这种逻辑只能用于更新界面,不能直接写入服务端权益:
if (purchaseState == PURCHASED) {
grantVip();
}
更安全的边界是:客户端获得凭证后立即发送到后端;后端验证商店状态,并把一次购买映射到明确账号、商品和权益记录。客户端收到后端的授权结果后再展示会员状态。这样即使本地显示逻辑被修改,真实服务仍受后端台账控制。
二、Play Billing服务端验单流程
推荐链路如下:用户发起购买,Play Billing返回purchaseToken,客户端将Token和业务上下文发送到后端,后端调用Google Play Developer API核验,写入幂等的购买记录,最后发放或更新权益。业务上下文可以包括内部账号、商品ID、应用版本和请求时间,但不要把客户端传来的字段未经校验就当成可信结论。
后端至少要核对以下项目:
| 核对项 | 目的 |
|---|---|
| purchaseToken是否存在且属于目标应用 | 排除伪造、错包和环境混用 |
| 商品ID、套餐、数量 | 防止低价商品映射高价值权益 |
| 购买状态 | PURCHASED才进入发放流程,PENDING保持待处理 |
| Token是否已处理 | 保证重试和重复提交不会重复发放 |
| 内部账号映射 | 防止把他人购买绑定到当前账号 |
| 退款、撤销、消耗状态 | 及时回收或停止继续使用权益 |
| linkedPurchaseToken | 处理订阅升级、降级和替换关系 |
Google官方安全建议要求后端核验购买并避免重复处理。实现时应把“已验证”“待支付”“已退款”“已撤销”“调查中”和“系统错误”分成不同状态,不能把API暂时不可用当作购买成功。网络超时可以安全重试,但发放操作必须有唯一约束或幂等键。
三、订阅、退款与权益台账
一次购买记录和一个长期权益不是同一对象。订阅续期、升级、降级、宽限期、退款和撤销都会改变用户资格。建议建立不可随意覆盖的权益台账:记录商店、Token引用、内部账号、商品、开始时间、到期时间、当前状态、最后核验时间和处理版本。敏感凭证应按最小权限保存,日志中只保留脱敏引用。
发生重复Token时,后端返回同一笔已确认结果,不重新增加时长或数量。发生退款或撤销时,按照产品规则回收未使用权益;不要因为客户端仍显示“会员中”就继续放行高价值接口。订阅替换要把旧Token和新Token放进同一条关系链,防止用户同时获得两份权益。
四、御盾在内购安全中的职责
御盾不是商店结算系统,不能替客户向Google或Apple证明交易,也不能替后端决定是否永久发放权益。御盾适合保护以下客户端环节:
- BillingClient、StoreKit和App Attest调用入口;
- Token提取、请求绑定和权益刷新调用链;
- 商品、账号和会话参数的组装逻辑;
- 本地完整性判断、版本检查和异常上报;
- 重打包、重签名、Hook和非官方客户端的风险信号;
- 与服务端挑战、限额和二次验证相关的关键逻辑。
保护的目标是提高修改和仿造成本,并让后端更容易获得可解释的上下文。若攻击者绕过了客户端调用,后端仍应要求有效交易凭证和正确账号映射。不能使用“加固后绝对无法盗刷”或“Play Integrity通过即绝对可信”这样的表述。
五、Play Integrity如何参与权益门禁
完整性信号应当作为风险输入,而不是唯一封禁条件。低风险正常购买可以快速确认;信号异常时可以要求重新挑战、延迟高价值权益、限制优惠、增加人工复核或降低接口额度。只有在账号、交易、设备、会话和历史行为共同支持时,才考虑阻断。
建议先观察真实流量和失败分布,再逐步启用强制策略。测试包、企业分发、地区渠道和Play外分发可能具有不同的合法身份,不能把所有非标准安装都直接判成欺诈。相关职责可参考应用真实性与接口安全,但平台信号仍需由客户后端解释。
六、iOS StoreKit 2的对应边界
iOS交易由App Store签名,StoreKit 2可提供交易信息,服务端还可以通过App Store Server API查询订阅、退款和撤销。JWS证明交易来源,但不等于当前客户端没有被修改,也不代表账号映射和权益台账已经正确。App Attest可以增加应用实例和请求证明,御盾可以保护关键调用链和运行时逻辑,最终权益仍由后端根据交易与业务规则发放。
Android和iOS可以使用统一的业务状态机,但商店凭证、订阅回调、测试环境和错误状态必须分别实现。不要把Google purchaseToken当作Apple交易ID,也不要把Sandbox结果直接当作Production结论。双端发布门禁至少应保留商店环境、应用版本、候选包Hash、验证状态、权益结果和回滚对象。
七、六组内购安全PoC
一份可审计的PoC应包含至少六组:正常购买、PENDING购买、重复提交Token、已退款购买、错误账号绑定、客户端本地购买状态被修改。每组记录原始Release、基础保护包、目标保护包、Billing或StoreKit版本、测试商品、后端验证结果、权益台账结果和未覆盖范围。
不要把“应用能启动”当作内购通过。还要验证恢复购买、账号切换、重装、覆盖升级、弱网重试、服务端超时、支付取消、订阅替换和高价值接口。若只测了Sandbox或测试轨道,应明确写成测试环境结论,不能外推为正式商店兼容。
八、发布门禁与工程落地
发布前冻结构建链、商品配置、签名责任和回滚包。门禁报告建议包括:App版本、Billing/StoreKit版本、原始包Hash、加固候选包Hash、最终签名包Hash、商店环境、已测账号、商品ID、Token脱敏引用、验证状态、Integrity/App Attest状态、权益结果、异常责任人和回滚条件。
CI/CD中可把“构建成功、安装成功、关键购买路径通过、后端验单通过、权益幂等通过、最终商店包通过”拆成独立状态。任何一项未知都不能自动标记为全部通过。对高频发版团队,建议把购买状态机和验单回归作为固定测试集,避免每次加固升级后只做一次手工点击。
九、常见误区
误区一:客户端收到PURCHASED就发放
PurchaseState只是客户端收到的状态,最终权益应由后端核验并写入台账。
误区二:加固可以替代服务端验单
加固保护调用链,不能证明商店交易真实,也不能替代退款和订阅管理。
误区三:异常Integrity信号直接永久封号
单一信号可能误伤测试包、渠道包或设备异常用户,应使用分级处置和复核。
误区四:只测新购,不测退款和重复提交
盗刷和权益漏洞通常出现在状态转换、重试、退款和账号映射,而不是首次购买按钮。
误区五:把测试环境结论写成正式兼容承诺
PoC必须写清商店环境、版本、设备、覆盖路径和未覆盖项。
十、FAQ
APP已经接入Play Billing,还需要加固吗?
需要视业务价值判断。Play Billing解决交易接入和商店凭证问题,御盾可以降低客户端关键链路被修改、仿冒和批量调用的成本;两者不能互相替代。
purchaseToken可以保存多久?
保存策略应由客户结合最小化、审计、退款和客服需求确定。生产日志不应直接打印完整Token,应使用受控的脱敏引用和访问审计。
Pending购买能不能先给部分权益?
只有在产品规则和风控允许时才可设计临时状态;不能把PENDING当成已完成支付,更不能发放不可回收的永久权益。
iOS和Android能共用一套验单代码吗?
可以共用业务状态机和台账模型,但商店API、凭证格式、订阅事件和测试环境必须分别实现并验证。
御盾是否保证绝对防盗刷?
不保证。御盾的价值是保护客户端和运行时关键链路、提供风险上下文并协助PoC归因;交易真实性、账号关系和权益最终决定仍由商店与客户后端负责。
结语与下一步
内购安全的核心不是把“发会员”这一行代码藏得更深,而是让客户端、商店、服务端和权益台账各自承担清晰责任。若你正在评估Android或iOS的会员、订阅、道具或AI额度安全,可从御盾APP加固产品页了解能力范围,并提交候选包与一条真实付费路径申请内购安全PoC。PoC会区分已验证、待验证和不适用内容,不以一次成功启动替代正式发布结论。
公开依据
- Google Play Billing安全
- Google Play Billing集成
- Google Play Developer API购买查询
- Apple StoreKit
- Apple App Store Server API
- Google Play Integrity
事实依据与脱敏证据
- Google Play Billing安全文档建议后端验证购买并避免重复处理。
- Google Play Developer API提供商品购买状态查询能力。
- Apple StoreKit提供签名交易信息,交易事实可供服务端核验。
- Apple App Store Server API用于查询订阅、退款和撤销状态。
- Play Integrity提供应用、安装和环境相关信号,需结合业务风险解释。
| 编号 | 依据 | 结论 |
|---|---|---|
| 1 | Google Play Billing安全文档 | 后端核验购买并防重复处理 |
| 2 | Google Play Developer API | 查询商品购买状态 |
| 3 | Apple StoreKit | 提供签名交易信息 |
| 4 | Apple App Store Server API | 同步订阅、退款和撤销 |
| 5 | Play Integrity | 提供应用和环境信号 |
| 来源或对象 | 可公开事实 | 本文用途 | 边界 |
|---|---|---|---|
| Google Play Billing安全文档 | 建议后端核验购买并防止重复处理 | 支持服务端验单结论 | 不代表客户系统已接入 |
| Google Play Developer API | 可查询商品购买状态 | 支持purchaseToken核验流程 | API故障需单独处理 |
| Apple StoreKit | 提供签名交易信息 | 支持iOS交易真实性说明 | 不替代账号权益台账 |
| Apple App Store Server API | 可从服务端查询购买状态 | 支持退款和订阅同步 | 需按生产环境配置 |
| Play Integrity | 提供应用、安装或环境信号 | 支持风险分级说明 | 不是唯一封禁条件 |
| 御盾公开产品边界 | 保护客户端关键链和运行时逻辑 | 说明产品职责 | 不承诺绝对安全 |
技术拆解
购买安全由四个对象组成:商店交易、客户端凭证、业务后端和权益台账。商店负责产生可验证交易事实;客户端负责采集并传输凭证;后端负责验证、幂等、账号映射和退款处理;台账负责表达当前可用权益。任何一层都不应把另一层的状态未经验证直接继承。Android和iOS可以共享状态机设计,但不能混用凭证格式、订阅事件和测试环境。
工程落地
建议先建立原始Release基线,再生成基础保护候选和目标保护候选,最后验证商店签名包。每一组保存版本、构建链、商品配置、候选包Hash、后端验证状态、权益结果和回滚对象。CI/CD将构建、安装、购买、验单、退款、重复提交和权益回收拆成独立门禁;任何未知状态都进入人工复核,不自动发放高价值权益。
攻防视角
攻击者可能修改本地购买回调、替换数据库、重放凭证、伪造账号映射或批量调用权益接口。客户端加固主要抬高补丁和仿冒成本,服务端验单负责阻止无效交易,账号与行为策略负责识别异常。防守方不能只依赖某个布尔值,也不能把异常设备信号直接等同欺诈;观察、挑战、限额、延迟和人工复核应形成分级动作。
风险边界
本文没有执行客户包、真实商店账号或生产交易测试。示例字段均为方法说明,不构成法律、财务或商店审核承诺。正式上线前应由客户确认商品、税务、退款、隐私、数据保存、客服和回滚责任。不得在公开页面放置完整purchaseToken、服务账号、私钥、客户订单或可运行的绕过代码。
主目标与非目标
本次文章主目标是建立可被研发、产品和安全团队采用的内购权益安全门禁模型。非目标包括:不声称御盾已通过所有Billing或StoreKit版本、不替代Google或Apple官方接入、不提供生产凭证、不把一份方法文档当作客户PoC结论。
| 目标项 | 需要验证的对象 | 状态表达 |
|---|---|---|
| 交易真实性 | 商店API与签名交易 | 已定义方法,客户项目待验证 |
| 幂等发放 | 重复Token和重试 | 已定义门禁,客户项目待验证 |
| 退款回收 | 退款、撤销和订阅替换 | 已定义状态,客户项目待验证 |
| 客户端保护 | 关键调用链和运行时逻辑 | 需候选包PoC |
事实依据与脱敏证据补充
公开依据只用于说明协议和责任边界,不等同于御盾已对每一种Billing、StoreKit、设备、商店或渠道组合完成实测。客户项目应在自己的测试商品、账号、地区和生产配置下复核。若官方API返回暂时不可用,系统应保留待处理状态,而不是向用户展示永久有效权益。若同一凭证重复到达,台账应返回原处理结果,不能新增一条发放记录。
购买状态与回滚说明
内购发布门禁还应覆盖账号切换、设备重装、覆盖升级、弱网重试、服务端超时、退款通知延迟、订阅升级和降级。每种场景都要记录客户端显示、后端状态、权益台账和最终接口结果。失败时保留可回滚版本,避免为了快速恢复而直接放开本地发放逻辑。对高价值权益,可先进入待确认状态,再根据后端核验和风险信号转为可用。
交付与转化边界
御盾PoC的交付物可以包括候选包身份表、调用链保护范围、商店验单字段表、权益状态机、异常处置矩阵和未覆盖清单。交付物不包含生产密钥、完整交易凭证、客户订单或可复用的绕过脚本。需要进一步评估时,请通过产品页提交Android或iOS Release候选包、一条付费业务路径和测试环境说明。
测评目标与非目标
本次安全评估的主目标是确认交易凭证、服务端验单、权益台账和客户端保护之间的责任边界。非目标是不对客户项目作绝对防盗刷承诺、不替代商店审核、不公开生产凭证、不把单一设备或完整性信号直接等同欺诈。
| 测评目标 | 观察对象 | 通过口径 |
|---|---|---|
| 正常购买链路 | 客户端、商店、后端和台账 | 只发放一次正确权益 |
| 重复凭证处理 | 同一Token重试与并发 | 幂等返回,不重复发放 |
| 退款与撤销 | 状态通知、台账和接口 | 权益按规则回收或冻结 |
| 客户端修改 | 本地状态和调用链 | 后端仍要求有效交易和账号映射 |
来源报告事实映射
| 报告事实 | 公开支持 | 对本文的作用 |
|---|---|---|
| 客户端购买状态不应直接发放权益 | Play Billing安全文档 | 说明责任分层 |
| Token需要服务端验证 | Developer API文档 | 说明验单步骤 |
| 订阅和退款会改变资格 | App Store Server API文档 | 说明台账同步 |
| iOS交易包含签名信息 | StoreKit文档 | 说明JWS边界 |
| 完整性信号需要业务解释 | Play Integrity文档 | 说明分级处置 |
字段动态时间线
购买事件从客户端发起、商店确认、凭证提交、后端核验、台账写入到权益可用,状态会随重试、退款、撤销和订阅替换变化。每个时间点应保存脱敏Token引用、应用版本、账号映射、验证时间和处理版本;不保存完整凭证到公开日志。