news 2026/8/22 14:50:00

如何防止内购破解与白嫖?flutter_inapp_purchase服务端收据验证完全指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
如何防止内购破解与白嫖?flutter_inapp_purchase服务端收据验证完全指南

如何防止内购破解与白嫖?flutter_inapp_purchase服务端收据验证完全指南

【免费下载链接】flutter_inapp_purchaseFlutter In App Purchase plugin that confirms OpenIAP项目地址: https://gitcode.com/gh_mirrors/fl/flutter_inapp_purchase

在 Flutter 应用中做内购(In-App Purchase),最容易被忽视的坑就是:只在客户端判断"用户是否买过",结果被破解者绕过,免费白嫖你的付费功能。本文将用 flutter_inapp_purchase 这个 OpenIAP 兼容的内购插件,手把手讲清楚服务端收据验证(Receipt Validation)的完整流程:iOS 的收据数据、Android 的 purchaseToken、第三方验证服务,以及一套防止白嫖的完整安全清单。

为什么客户端验证防不住白嫖?

很多新手会这样写逻辑:用户点击购买 → 收到"购买成功"事件 → 直接解锁付费功能。这个流程在正常设备上没问题,但存在两个致命漏洞:

  • 重放与伪造:客户端代码运行在用户设备上,攻击者可以通过 Hook、重放旧的"购买成功"事件,让 App 误以为完成了付费;
  • 跳过购买流程:只要解锁状态存在本地(比如 SharedPreferences 里存了个isVip = true),改一个字节就能永久免费使用。

唯一可靠的防线是:由你的后端服务器,向 Apple / Google 官方验证收据的真实性,只有服务器确认通过后才下发权益。这就是"服务端收据验证"的核心思想。

flutter_inapp_purchase 在设计上已经为这条防线做好了准备:所有购买事件都会携带可验证的凭证字段,验证失败前你绝不应该发货。

两个平台,两种验证凭证

iOS 和 Android 的收据格式完全不同,但插件把它们统一收敛到了Purchase对象上(定义见lib/types.dart,事件分发逻辑在lib/flutter_inapp_purchase.dart):

对比项iOS / macOSAndroid
核心凭证收据数据(transactionReceipt / JWS 字符串)purchaseToken(购买令牌)
官方验证服务App Store Server API / verifyReceiptGoogle Play Developer API
验证发起方你的后端(用 Apple 公钥或 Apple 接口)你的后端(OAuth 2.0 服务账号)
关键方法validateReceiptIOS()getReceiptDataIOS()verifyPurchase()(google 参数)

📌 记住一句话:iOS 验"收据",Android 验"令牌",但都必须在你自己的服务器上完成。

第一步:拿到可验证的收据数据

无论哪个平台,第一步都是把凭证从插件里取出来。有两种方式:

1. 监听购买事件(新购买时触发)

通过purchaseUpdatedListener流,每笔新交易都会带一个Purchase对象,其中包含:

  • productId:产品 ID(SKU)
  • transactionId:交易唯一标识
  • transactionReceipt(iOS)/purchaseToken(Android):服务端验证用的凭证

2. 拉取已有购买(App 启动/恢复购买时)

调用getAvailablePurchases(),插件会自动向商店查询该用户当前所有有效权益——iOS 底层对应 StoreKit 2 的Transaction.currentEntitlements,Android 会自动合并查询一次性产品和订阅两个列表。这一步既能做"恢复购买",也是防止用户换机/重装后凭证丢失的关键。

final purchases = await iap.getAvailablePurchases(); for (final p in purchases) { // iOS: p.transactionReceipt(JWS) // Android: p.purchaseToken // 把凭证发到你的服务器去验证,而不是在本地解锁功能! }

iOS:收据验证的正确姿势

iOS 端插件提供了validateReceiptIOS()方法(源码实现见ios/Classes/FlutterInappPurchasePlugin.swift),它基于 StoreKit 2 完成本地初步校验,并返回一个包含jwsRepresentation的结果对象——这个 JWS 字符串就是你应该发给服务器的凭证

⚠️ 注意:插件文档中明确标注本地校验仅用于测试(LOCAL TESTING ONLY)。生产环境的流程应该是:

  1. 客户端把jwsRepresentation(或transactionReceipt)连同产品 ID 发给你的后端;
  2. 后端用 Apple 的公钥验证 JWS 签名,或调用 Apple 官方验证接口;
  3. 验证响应中status === 0表示收据有效,还要检查交易状态(是否已退款revoked、是否过期);
  4. 后端确认后,向客户端返回签名过的"权益凭证",客户端凭此解锁功能。

另外,沙盒测试收据与正式收据的环境不同,服务端要处理好21007(沙盒收据发到正式接口)/21008(正式收据发到沙盒接口)这类状态码,自动切换验证端点重试。

Android:purchaseToken + 官方开发者 API

Android 端不走"收据"概念,而是每笔购买对应一个 purchaseToken。插件在 Android 侧已自动完成购买数据的校验与合并(实现见android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt),并把purchaseToken暴露给 Dart 层。

服务端验证流程:

  1. 用 Google 服务账号走 OAuth 2.0 获取 access token(只放在服务器上,绝不能打进客户端);
  2. 后端调用 Google Play Developer API,用packageName + productId + purchaseToken查询这笔购买;
  3. 一次性产品:检查purchaseState(0 表示已购买);
  4. 订阅:还要检查expiryTimeMillis是否已过期、是否处于宽限期。

插件也提供了verifyPurchase()方法配合google参数(传入 packageName 和 purchaseToken 等),方便你在调试期快速打通验证链路,但同样建议生产环境走你自己的后端。

💡 补充一点:插件遵循 OpenIAP 开放内购标准,除了 Apple / Google,其verifyPurchase()还提供horizon参数,可面向 Horizon OS 等更多平台扩展统一的验证模型。

不想自己写后端?用第三方验证服务

自己实现双端签名验证、处理退款和宽限期状态其实工作量不小。插件内置了verifyPurchaseWithProvider()方法,可以直接对接第三方验证服务(如 IAPKit),把 JWS / purchaseToken 发给它做权威校验,返回统一的状态:

验证状态含义你应该怎么做
entitled权益有效解锁功能
expired已过期撤销权益
canceled用户取消(可能仍有剩余时间)展示续费引导
pending-acknowledgment待确认(Android)调用finishTransaction()
inauthentic验证失败,可能是伪造拒绝发货并记录日志

对于小团队,这是一条"低成本拿到企业级验证"的捷径;对于有后端团队的项目,自建验证服务则能完全掌控数据。

防止白嫖的 7 条安全清单

结合 收据验证 API 文档 的最佳实践,上线前请逐条自查:

  1. 验证通过前绝不发货——解锁状态以服务器为准,客户端只做展示;
  2. 客户端验证仅用于开发调试,生产必须走服务器;
  3. 验证成功后再调用finishTransaction()——iOS 用它出队,Android 对消费型产品会消耗、对非消费型产品会确认;Android 必须在 3 天内确认,否则商店会退款;
  4. 凭证不要落本地明文存储——收据/token 存服务器,本地只存"服务器签发的权益";
  5. 密钥永远只在服务端——iOS 的 shared secret、Google 的 OAuth 服务账号都不能出现在客户端代码里;
  6. 处理退款与订阅生命周期——revoked(退款)、inGracePeriod(宽限期)状态要正确回收权益;
  7. App 启动时重新对账——尤其 Android:订阅在 App 关闭期间续费不会触发purchaseUpdatedListener事件,必须主动调用getAvailablePurchases()并向服务器重新验证,否则用户会"莫名失去会员"。

常见疑问 FAQ

Q:我已经调用了validateReceiptIOS()且返回有效,还需要服务器验证吗?需要。本地校验只能验证 JWS 是否来自 Apple 签名,无法覆盖退款、取消、过期等完整业务状态,且本地逻辑可被绕过。它适合开发调试,生产请以服务器结论为准。

Q:验证失败了还能重试吗?能。保持交易不 finish,在purchaseErrorListener中捕获错误(错误码定义见lib/enums.dart),对网络类错误做指数退避重试即可。

Q:一次性产品和订阅的验证区别是什么?一次性产品验证后关注"是否已消费"(Android 消费后可重复购买);订阅还要持续跟踪续费、到期、宽限期状态,推荐配合getActiveSubscriptions()和 iOS 的subscriptionStatusIOS()获取细分阶段(试用期、宽限期等)。

总结

防止内购破解的公式其实很简单:客户端只负责"把凭证交出去",服务器负责"验真伪、发权益",iOS 交 JWS 收据、Android 交 purchaseToken,验证通过才 finish 交易。flutter_inapp_purchase 已经把这些凭证和验证接口全部准备好——getAvailablePurchases()取证、validateReceiptIOS()调 iOS 链路、verifyPurchase()打通 Android 链路、verifyPurchaseWithProvider()一键接入第三方验证。把这套流程跑通,你的付费功能就基本告别白嫖了。🔒

【免费下载链接】flutter_inapp_purchaseFlutter In App Purchase plugin that confirms OpenIAP项目地址: https://gitcode.com/gh_mirrors/fl/flutter_inapp_purchase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/22 14:47:42

135、洞察驱动的实战标题——色调映射的“审美偏差“——日系/欧系/国产手机的影调差异来源,从直方图塑形到局部对比度控制的调参心法

135、洞察驱动的实战标题——色调映射的"审美偏差"——日系/欧系/国产手机的影调差异来源,从直方图塑形到局部对比度控制的调参心法 上周三晚上十一点,我盯着两台旗舰机的同场景夜景样张,差点把咖啡泼在键盘上。一台是日系某大厂,一台是欧系某老牌,同一颗IMX传…

作者头像 李华
网站建设 2026/8/22 14:47:15

MOFA2 完整指南:3 步跑通你的第一个多模态因子分析

MOFA2 完整指南:3 步跑通你的第一个多模态因子分析 【免费下载链接】MOFA2 Multi-Omics Factor Analysis 项目地址: https://gitcode.com/gh_mirrors/mo/MOFA2 MOFA2 是一款基于 Python 的生物数据分析工具,专门面向多模态、混合类型数据&#xf…

作者头像 李华