Android/Google Play 应用内订阅是否需要自有服务器?
结论
不需要部署自有服务器,也能完成 Google Play 订阅购买流程。Android 应用可以只使用 Google Play Billing Library 发起购买、读取订阅状态并确认购买。
如果订阅涉及付费内容、会员权限或其他有价值的服务,生产环境应使用可信后端验证 purchaseToken 并管理订阅权益。客户端返回的购买信息不能作为可靠的最终凭证,Google Play Developer API 使用的服务端凭据也不能放进 APK。
可以按以下情况选择:
- 个人项目、原型或低风险功能,可以只在客户端实现。
- 正式商业应用,应通过后端验证购买并维护权益。
- 如果需要跨设备权益、退款或撤销处理、账号体系以及服务端内容,通常需要后端。
为什么客户端可以购买,却不适合独立验证
Google Play Billing Library 负责处理客户端购买流程,包括:
- 查询可售商品;
- 打开 Google Play 购买界面;
- 获取
Purchase和purchaseToken; - 确认购买;
- 查询当前账号在当前设备环境下的购买记录。
所以,即使没有自有服务器,用户也能完成订阅。
但客户端运行在用户控制的设备上。设备经过 root,或者有人使用 Hook、修改 APK、伪造运行环境,都可能绕过客户端校验。签名校验和代码混淆可以增加攻击难度,却无法让客户端成为可信环境。
Google Play Developer API 是服务端接口,调用时通常需要安全保存服务账号或 OAuth 凭据。如果把这些凭据写入 APK,攻击者可能从安装包或运行时中将其提取出来。因此,Android 应用不应直接携带服务端凭据调用该 API。
不使用服务器时的实现方式
纯客户端方案通常按以下步骤实现:
- 使用
BillingClient连接 Google Play。 - 查询订阅商品及其
ProductDetails。 - 调起购买流程。
- 在
PurchasesUpdatedListener中处理购买结果。 - 确认状态为
Purchase.PurchaseState.PURCHASED的购买。 - 应用启动或恢复时调用
queryPurchasesAsync(),重新计算本地权益。
下面的简化示例省略了界面状态管理和异常重试:
private lateinit var billingClient: BillingClient
private val purchasesUpdatedListener =
PurchasesUpdatedListener { billingResult, purchases ->
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
purchases.orEmpty().forEach { purchase ->
handlePurchase(purchase)
}
} else if (
billingResult.responseCode !=
BillingClient.BillingResponseCode.USER_CANCELED
) {
// 记录错误并按业务需要重试或提示用户
}
}
fun initializeBilling() {
billingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases()
.build()
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(result: BillingResult) {
if (result.responseCode ==
BillingClient.BillingResponseCode.OK
) {
restoreSubscriptions()
}
}
override fun onBillingServiceDisconnected() {
// 使用退避策略重新连接
}
})
}
处理购买时,应先检查购买状态。纯客户端模式可以根据该状态临时授予本地权益:
private fun handlePurchase(purchase: Purchase) {
when (purchase.purchaseState) {
Purchase.PurchaseState.PURCHASED -> {
if (!purchase.isAcknowledged) {
val params = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
billingClient.acknowledgePurchase(params) { result ->
if (result.responseCode ==
BillingClient.BillingResponseCode.OK
) {
grantLocalEntitlement(purchase)
}
}
} else {
grantLocalEntitlement(purchase)
}
}
Purchase.PurchaseState.PENDING -> {
// 不要授予正式权益,等待购买状态变为 PURCHASED
}
else -> {
// 未完成购买,不授予权益
}
}
}
应用重新启动或回到前台时,需要恢复订阅:
private fun restoreSubscriptions() {
val params = QueryPurchasesParams.newBuilder()
.setProductType(BillingClient.ProductType.SUBS)
.build()
billingClient.queryPurchasesAsync(params) { result, purchases ->
if (result.responseCode ==
BillingClient.BillingResponseCode.OK
) {
purchases
.filter {
it.purchaseState ==
Purchase.PurchaseState.PURCHASED
}
.forEach(::handlePurchase)
}
}
}
发起订阅购买时,应使用查询得到的 ProductDetails 和相应的 SubscriptionOfferDetails:
fun launchSubscription(
activity: Activity,
productDetails: ProductDetails,
offerToken: String
) {
val productParams =
BillingFlowParams.ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(offerToken)
.build()
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(listOf(productParams))
.build()
billingClient.launchBillingFlow(activity, billingFlowParams)
}
具体构造方式可能随 Google Play Billing Library 的版本变化,应以项目所用版本的官方 API 为准。
推荐的后端验证流程
更可靠的设计如下:
Android 应用
│
│ productId、purchaseToken、用户身份
▼
业务后端
│
│ 使用保存在服务端的授权凭据
▼
Google Play Developer API
│
│ 返回订阅状态
▼
业务后端更新权益
一般按以下流程处理:
- 客户端完成购买并取得
purchaseToken。 - 客户端将
purchaseToken、商品标识和应用内用户身份发送给后端。 - 后端通过 Google Play Developer API 查询订阅。
- 后端校验包名、商品、购买状态和到期时间等信息。
- 后端将该 token 与应用账号绑定,防止同一个 token 被多个账号重复使用。
- 验证成功后授予权益。
- 后端继续处理续订、取消、退款、撤销、暂停和宽限期等状态变化。
- 客户端向后端查询权益,不再只依赖本地布尔值。
服务端只需保存 purchaseToken 和订阅状态,不应保存用户的支付卡信息。
可以使用托管服务代替自建服务器
需要后端,并不意味着一定要自行购买和维护一台长期运行的服务器。还可以选择:
- Cloud Functions、Cloud Run 等无服务器计算服务;
- 现有的业务 API;
- 提供 Google Play 订阅验证和权益管理的第三方平台。
重点不在服务器由谁维护,而在于验证逻辑和 Google API 凭据必须放在客户端无法控制的可信环境中。
需要特别注意的问题
必须确认购买
购买完成后,应及时调用 acknowledgePurchase()。没有按照 Google Play 的要求确认购买,可能导致交易被自动退款。购买处于 PENDING 状态时,不要确认购买,也不要授予正式权益。
如果商品是可消耗的一次性商品,应执行消费操作。订阅通常需要确认,而不是消费。
查询购买记录不等于管理完整的订阅生命周期
queryPurchasesAsync() 可以恢复客户端当前可见的购买记录,但不能完全代替服务端状态管理。例如:
- 用户可能在应用没有运行时取消或续订;
- 购买可能被退款或撤销;
- 支付可能进入宽限期、暂停或账号保留状态;
- 本地缓存可能过期或遭到篡改;
- 用户可能更换设备,或需要跨平台使用权益。
如果这些情况会影响服务端资源访问,就应由后端维护最终权益状态,并根据需要接收 Real-time Developer Notifications。
不要从 Android 客户端直接调用 Developer API
即使客户端能够发送 HTTP 请求,也不应把服务账号密钥、刷新令牌或其他长期凭据放进应用。APK 可以被反编译,运行时数据也可能被截取。
Google Play Billing Library 和 Google Play Developer API 负责不同的环节:
- Billing Library 用于在客户端展示商品并完成购买交互。
- Developer API 用于在可信服务端验证和管理购买状态。
如何选择
如果订阅只用于解锁单机应用中的低价值功能,并且可以接受破解风险,那么纯客户端方案能够工作,但仍需正确处理购买确认、待处理交易和购买恢复。
如果订阅控制会员内容、云端数据、API 调用、跨设备功能或实际商业权益,就应使用后端。客户端负责完成购买并上传 purchaseToken,后端负责验证购买,并决定用户是否拥有相应权益。
备注:内容仅供参考。
