PAYATHON 2026

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。

不使用服务器时的实现方式

纯客户端方案通常按以下步骤实现:

  1. 使用 BillingClient 连接 Google Play。
  2. 查询订阅商品及其 ProductDetails。
  3. 调起购买流程。
  4. 在 PurchasesUpdatedListener 中处理购买结果。
  5. 确认状态为 Purchase.PurchaseState.PURCHASED 的购买。
  6. 应用启动或恢复时调用 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
    │
    │ 返回订阅状态
    ▼
业务后端更新权益

一般按以下流程处理:

  1. 客户端完成购买并取得 purchaseToken。
  2. 客户端将 purchaseToken、商品标识和应用内用户身份发送给后端。
  3. 后端通过 Google Play Developer API 查询订阅。
  4. 后端校验包名、商品、购买状态和到期时间等信息。
  5. 后端将该 token 与应用账号绑定,防止同一个 token 被多个账号重复使用。
  6. 验证成功后授予权益。
  7. 后端继续处理续订、取消、退款、撤销、暂停和宽限期等状态变化。
  8. 客户端向后端查询权益,不再只依赖本地布尔值。

服务端只需保存 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,后端负责验证购买,并决定用户是否拥有相应权益。

备注:内容仅供参考。