如何防止内购破解与白嫖?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 / macOS | Android |
|---|---|---|
| 核心凭证 | 收据数据(transactionReceipt / JWS 字符串) | purchaseToken(购买令牌) |
| 官方验证服务 | App Store Server API / verifyReceipt | Google 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)。生产环境的流程应该是:
- 客户端把
jwsRepresentation(或transactionReceipt)连同产品 ID 发给你的后端; - 后端用 Apple 的公钥验证 JWS 签名,或调用 Apple 官方验证接口;
- 验证响应中
status === 0表示收据有效,还要检查交易状态(是否已退款revoked、是否过期); - 后端确认后,向客户端返回签名过的"权益凭证",客户端凭此解锁功能。
另外,沙盒测试收据与正式收据的环境不同,服务端要处理好21007(沙盒收据发到正式接口)/21008(正式收据发到沙盒接口)这类状态码,自动切换验证端点重试。
Android:purchaseToken + 官方开发者 API
Android 端不走"收据"概念,而是每笔购买对应一个 purchaseToken。插件在 Android 侧已自动完成购买数据的校验与合并(实现见android/src/main/kotlin/io/github/hyochan/flutter_inapp_purchase/AndroidInappPurchasePlugin.kt),并把purchaseToken暴露给 Dart 层。
服务端验证流程:
- 用 Google 服务账号走 OAuth 2.0 获取 access token(只放在服务器上,绝不能打进客户端);
- 后端调用 Google Play Developer API,用
packageName + productId + purchaseToken查询这笔购买; - 一次性产品:检查
purchaseState(0 表示已购买); - 订阅:还要检查
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 文档 的最佳实践,上线前请逐条自查:
- ✅验证通过前绝不发货——解锁状态以服务器为准,客户端只做展示;
- ✅客户端验证仅用于开发调试,生产必须走服务器;
- ✅验证成功后再调用
finishTransaction()——iOS 用它出队,Android 对消费型产品会消耗、对非消费型产品会确认;Android 必须在 3 天内确认,否则商店会退款; - ✅凭证不要落本地明文存储——收据/token 存服务器,本地只存"服务器签发的权益";
- ✅密钥永远只在服务端——iOS 的 shared secret、Google 的 OAuth 服务账号都不能出现在客户端代码里;
- ✅处理退款与订阅生命周期——
revoked(退款)、inGracePeriod(宽限期)状态要正确回收权益; - ✅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),仅供参考