BlogRight arrowTutorialRight Arrow安卓应用内购买,第5部分:服务器端购买验证
BlogRight arrowTutorialRight Arrow安卓应用内购买,第5部分:服务器端购买验证

安卓应用内购买,第5部分:服务器端购买验证

安卓应用内购买,第5部分:服务器端购买验证
Listen to the episode
安卓应用内购买,第5部分:服务器端购买验证

服务器端(server-side)验证可以帮助您验证购买的真实性。设备将向Google服务器发出请求,以查明购买是否已实际发生,以及购买是否有效。

在本指南中,我们将讨论如何配置安卓应用程序的服务器端验证。

为什么需要验证购买

需要注意的是,服务器端验证不是强制性的——没有它,应用内购买仍然有效。不过也有一些显著的好处:

  1. 高级支付分析,这对订阅尤其重要,因为激活后发生的一切都不会被设备处理。如果没有服务器端购买处理,您将无法检索当前的订阅状态,也无法了解用户是续订了还是取消了订阅,是否存在任何支付问题,等等。
  2. 能够验证购买的真实性。您将确保交易不是欺诈,用户确实已经为您的产品付费。
  3. 跨平台订阅。如果能够实时查看用户的订阅状态,则可以与其他平台进行同步。例如,从iOS设备上购买订阅的用户将能够在安卓、网络和其他平台上使用。 
  4. 能够从服务器端控制内容访问,通过简单地执行对服务器的请求来保护您不受那些未订阅却试图访问数据的用户的影响。 

从我们的经验来看,第一个优势本身就足够建立服务器端购买处理。

⭐️ Download our guide on in-app techniques which will make in-app purchases in your app perfect

支付验证

我们可以总结使用该方案的安卓支付验证:

对Google Play开发人员API请求的认证

如需使用Google Play开发人员API,您首先需要生成一个密钥来签名请求。首先,您必须将您的Google Play Console账户(在那里您管理您的应用程序)链接到您的谷歌云(Google Cloud)账户(在那里您将生成一个请求签名的密钥)。配置好一切后,您必须授予用户购买管理权限。这需要一篇专门的文章来描述这个过程。幸运的是,我们已经在Adapty文档中的步进式指南中介绍过了。 

请注意,在生成密钥后,通常需要等待24小时或更长时间才能开始工作。为了避免这种情况,只需更新任何应用内产品或订阅的描述就能立即激活密钥。 

我们使用官方的google-api-python-client库来使用Google Play开发人员API。这个库可用于大多数流行语言,我建议使用它,因为它支持您可能需要的所有方法。

订阅交易验证

iOS服务器端验证不同,在安卓中,订阅和其他产品验证都是使用多种方法实现的。因此,在验证交易时,您需要知道处理的是产品还是订阅。在实践中,这意味着您将需要从移动应用程序传输数据,以及在数据库中保留标志,以防需要重新验证令牌。

第二个重要的区别是,虽然在安卓中每笔交易都有自己的令牌,但所有iOS交易都使用一个特定于应用程序的共享密钥(shared secret)来存储整个交易历史。这意味着,如果您希望能够随时恢复用户的购买,您需要存储所有购买令牌,而不是选择任意的单个。

如需验证订阅,需要调用purchases.subscriptions.get方法。基本上,它是一个GET请求调用:

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}

所有参数都是必需的:

  • packageName:应用程序标识符,例如com.adapty.sample_app。
  • subscriptionId:要验证的订阅的订阅标识符,例如com.adapty.sample_app.weekly_sub。
  • token:独特的交易令牌。一旦在移动应用程序端处理了购买,它就会出现。

首先,让我们看看需要注意的错误消息,以确保一切都能按预期进行:

  • 400,Invalid grant: account not found:此错误消息意味着请求验证密钥生成错误。请确保您的账户是关联的,您使用的是有足够权限的正确账户,并且所有必需的API都已被激活。有关如何配置所有内容的指南,请参阅下面的小节。请注意关于产品描述更新的提示。
  • 400,The purchase token does not match the package name:通常在诈骗性交易中会遇到此错误消息。如果您在测试时看到它,请确保您没有使用属于不同应用程序的应用购买令牌。
  • 403,Quota exceeded for quota metric 'Queries' and limit 'Queries per day' of service 'androidpublisher.googleapis.com':这意味着已超过Google API请求的每日配额。默认情况下,每天最多可执行20万个请求。这个配额可以增加,但对大多数应用来说应该足够了。如果您遇到这个限制,您应该再次检查您的应用程序的逻辑,确保一切都是正确的。
  • 410,The subscription purchase is no longer available for query because it has been expired for too long:此错误消息出现在订阅已过期60天以上的交易中。这不是一个实际的错误消息,不应该这样处理。
Start for free

You don't need to write server code yourself,

because we did it for you. Try Adapty SDK!

Start for free

订阅交易 

如果验证成功,您将接收交易数据作为响应。

交易数据(用于订阅):

{
    "expiryTimeMillis": "1631116261362",
    "paymentState": 1,
    "acknowledgementState": 1,
    "kind": "androidpublisher#subscriptionPurchase",
    "orderId": "GPA.3382-9215-9042-70164",
    "startTimeMillis": "1630504367892",
    "autoRenewing": true,
    "priceCurrencyCode": "USD",
    "priceAmountMicros": "1990000",
    "countryCode": "US",
    "developerPayload": ""
}

如需了解用户是否可以访问应用程序提供的高级选项,即他们是否有活跃订阅,您需要:

  1. 检查startTimeMillisexpiryTimeMillis参数。当前时间应该在这两者之间。
  2. 此外,您还必须确保paymentState参数没有“0”值。这意味着订阅购买仍然悬而未决,因此,现在还没有必要授予用户高级功能访问权限。 
  3. 如果交易具有autoResumeTimeMillis属性,则暂停订阅。这意味着在指定日期之前不应该授予用户高级功能访问权。

让我们看看订阅交易的关键属性:

  • kind:交易类型。关于订阅,它总是有androidpublisher#subscriptionPurchase值。通过这个参数,您可以了解是在处理订阅还是产品,并相应地选择处理逻辑。
  • paymentState:付款状态。过期交易不存在此属性。可能的值是:
    0本次购买还没有被处理。在一些国家,用户可以现场支付订阅费用。也就是说,用户将从他们的设备启动订阅购买,并在附近的终端支付。总的来说,这是一个非常罕见的情况,但您仍然应该记住。
    1订阅已购买。
    2订阅还在试用期(trial)内。
    3订阅将在下一时期上调或下调。这意味着订阅计划需要更改。
  • acknowledgementState:购买确认状态。这是一个重要的参数,可以确认用户是否获得了他们所支付内容的访问权。“0”表示没有,“1”表示有。开发人员负责定义这个状态,这在移动应用程序和服务器端都可以完成。如果您在购买后3天内不确认,将自动退款。我建议实现这种逻辑:一旦接收到包含acknowledgementState=0的交易,服务器就会更改该参数。我将在下面说明如何做到这一点。 
  • orderId:独特的交易标识符。每个订阅购买或续订都有自己的标识符,可以使用该标识符来了解是否已经在早些时候处理过该交易。每个续订标识符都有一个固定的前半部分,后面会追加两个点和续订(subscription renewal)次数(以0开始)。如果订阅激活时具有GPA.3382-9215-9042-70164标识符,那么第一次续订将被识别为GPA.3382-9215-9042-70164..0,第二个是GPA.3382-9215-9042-70164..1,等等。通过这种方式,您可以构建交易链并跟踪续订次数。
  • startTimeMillis:订阅开始日期。
  • expiryTimeMillis:订阅过期日期。
  • autoRenewing:显示订阅是否要续订到下一阶段的标志。
  • priceCurrencyCode:三字母格式的购买货币,例如USD。
  • priceAmountMicros:买价。要得到正常的价格值,请将该值除以1000000。也就是说,1990000实际上意味着1.99。 
  • countryCode:两字母格式的购买国家,例如US。
  • purchaseType:购买类型。这个密钥在大多数情况下都不存在。它仍然很重要,因为它可以帮助您了解是否在沙盒环境中进行购买。可能的值是:
    0:购买是在沙盒环境中完成的,因此,它不应该包含在分析数据中。
    1:这次购买是用优惠码完成的。 
  • autoResumeTimeMillis:续订日期。它只出现在之前暂停的订阅中。如果存在此参数,则不需要在给定日期之前授予用户高级功能访问权。
  • cancelReason:不续订的原因。可能的值是:
    0:用户取消订阅自动续订。
    1:订阅被系统取消。这通常是由计费问题(billing issue)引起的。
    2用户切换到不同的订阅计划。
    3开发人员取消了订阅。
  • userCancellationTimeMillis:订阅续订取消(subscription renewal cancellation)数据。只有当cancelReason为0时,它才会出现。订阅仍然可以是活跃的——要确保这一点,请参阅expiryTimeMillis参数的值。
  • cancelSurveyResult:存储订阅取消(subscription cancellation)背后原因的对象,如果用户对该问题留下任何反馈,将显示该原因。
  • introductoryPriceInfo:存储介绍性价格数据的对象。例如,这可以是1个月的五折特价优惠。
  • promotionType:用于激活订阅的优惠码类型。可能的值是:
    0:一次性优惠码。
    1:可由多个客户运用的自定义优惠码。这类代码通常用于博客伙伴关系。 
  • promotionCode:用于激活订阅的自定义优惠码。一次性优惠码不存在此参数。 
  • priceChange:存储未来价格变化数据,以及用户是否同意该数据的对象。 

订阅确认

如前所述,如果在购买后3天内仍未确认订阅,它会被自动取消并退款。。说实话,我不太理解这背后的逻辑,我也从未在任何其他支付处理系统中遇到过这种情况,包括iOS系统。不过,如果您收到一个包含acknowledgementState=0的交易,您就要确认订阅。

为此,您需要调用purchases.subscriptions.acknowledge方法。这个方法执行了一个POST请求

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/subscriptions/{subscriptionId}/tokens/{token}:acknowledge

参数与请求验证相同。如果成功执行请求,订阅将得到确认,这意味着您不会损失钱。 

如果订阅还没有被完全购买,没有必要确认。 

订阅续订取消、撤销、退款和延期

除了订阅验证和确认外,Google Play开发人员API还可用于其他订阅操作。应该注意的是,这些是相当罕见的,除了续订外,均由Google Play Console支持。我仍会列出它们,让您对API解决方案的范围有一个大致的了解。所有这些请求都需要与前面提到的方法相同的参数(即packageNamesubscriptionIdtoken)。

  • 续订取消。purchases.subscriptions.cancel方法。它会取消所选订阅的自动续订。但是,订阅在当前计费期间仍然可用。
  • 订阅退款(Subscription refund)。purchases.subscriptions.refund方法。它会为订阅退款。但用户仍将保留订阅访问权,并将在下一阶段自动续订。在大多数情况下,您还应该在发出退款时撤销订阅。
  • 订阅撤销(subscription revocation)。purchases.subscriptions.revoke方法。它会立即取消订阅,这意味着用户将无法访问高级功能。订阅不续订。此方法通常与发出退款一起调用。
  • 订阅购买延期(subscription purchase deferral)。Purchases.subscriptions.defer方法。它将订阅扩展到指定日期。在请求中,指定订阅到期日期以及要替换它的日期。后者必须导致比前者更长的订阅期。

产品(不是订阅)验证

产品验证类似于订阅验证。您需要调用purchases.products.get方法来执行GET请求。

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}

通过查看上面列出的示例,我们已经熟悉了所有这些参数。

交易数据(用于订阅):

{
  "purchaseTimeMillis": "1630529397125",
  "purchaseState": 0,
  "consumptionState": 0,
  "developerPayload": "",
  "orderId": "GPA.3374-2691-3583-90384",
  "acknowledgementState": 1,
  "kind": "androidpublisher#productPurchase",
  "regionCode": "RU"
}

产品交易包含的属性比订阅交易要少得多。让我们来看一些重要的:

  • kind:交易类型。在产品中,它总是有androidpublisher#productPurchase值。通过这个参数,您可以了解是在处理订阅还是产品,并相应地选择处理逻辑。
  • purchaseState:付款状态。请注意,这里的键值与订阅的paymentState参数不同。可能的值是:
    0:购买已完成。
    1:购买已取消。这意味着该购买正在等待,但用户从未为其付费。
    2购买正在等待。在一些国家,用户可以现场支付订阅费用。也就是说,用户将从他们的设备启动订阅购买,并在附近的终端支付。总的来说,这是一个非常罕见的情况,但您仍然应该记住。
  • acknowledgementState:购买确认状态。这是一个重要的参数,可以确认用户是否获得了他们所支付内容的访问权。“0”表示没有,“1”表示有。开发人员负责定义这个状态,这在移动应用程序和服务器端都可以完成。如果您在购买后3天内不确认,将自动退款。我建议实现这种逻辑:一旦接收到包含acknowledgementState=0的交易,服务器就会更改该参数。我将在下面讨论如何做到这一点。 
  • consumptionState:产品消费状况。这就是iOS所称的“消费品”。它是在移动应用程序端定义的。如果它的值为“0”,则意味着产品没有被消费;如果它是“1”,则已被消费。如果您出售的是应用的终身(lifetime)访问权或某些特殊的高级功能,那么这样的产品就不应该被消费,也就是说,它们应该具有“0”的状态。如果您出售的是用户可以多次购买的货币,那么这种产品就应该被消费,也就是说,它们应该具有“1”的状态。consumptionState=0表示该产品只能购买一次,而consumptionState=1表示可多次购买该产品。
  • orderId:独特的交易标识符。每个订阅购买或续订都有自己的标识符,可以使用该标识符来了解是否已经在早些时候处理过该交易。
  • purchaseTimeMillis:购买日期。
  • regionCode:两字母格式的购买国家,例如US。请注意,此参数的名称与订阅中的名称不同,订阅中的名称为countryCode。 
  • purchaseType:购买类型。这个密钥在大多数情况下都不存在,但它仍然很重要,因为它可以帮助您了解是否在沙盒环境中进行购买。可能的值是: 

0: 购买是在沙盒环境中完成的,因此,它不应该包含在分析数据中。
1: 这次购买是用优惠码完成的。
2: 为目标操作授予购买,例如观看应用内部广告来代替付费。

如您所见,产品验证与订阅验证非常相似。不过,还有几点需要注意:

  • 虽然价格不退,但这对分析来说非常方便。
  • purchaseState参数的值与订阅中发现的paymentState参数的值有显著不同。如果不加以解释,会导致bug。 
  • regionCode被返回,即使订阅的名字是countryCode

就像订阅购买一样,产品购买也需要得到确认。为此,调用purchases.products.acknowledge方法,该方法将执行一个POST请求

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/products/{productId}/tokens/{token}:acknowledge

如果购买还没有完成,就没有必要确认了。

订阅和产品的退款跟踪

如果不考虑退款,就不可能有高质量分析。不幸的是,退款数据既没有出现在交易中,也没有作为一个单独的事件(event)提示,因为它在iOS中工作。如需接收退款交易列表,您需要定期调用purchases.voidedpurchases.list——例如,每天调用一次。这个方法将执行一个GET请求:

https://androidpublisher.googleapis.com/androidpublisher/v3/applications/{packageName}/purchases/voidedpurchases

作为对请求的响应,您会收到所有已退款交易的列表。我建议通过orderId参数在数据库中搜索交易,而不是通过purchaseToken参数。首先,这花费的时间更少。其次,所有续订都将共享同一个令牌,您只需要获取最近的一个即可。

交易的服务器通知(Server notification)

服务器通知(实时开发人员通知)可以帮助您了解在您的服务器上,Google端发生的几乎是实时的事件。一旦这些配置完成,您就会收到关于新购买、续订、付款问题等的通知。这可以帮助您收集更好的分析,并让订阅者状态管理更容易。

如需开始接收服务器通知,您需要创建一个Google Cloud Pub/Sub主题,它会向您所需的地址发送通知。应该在Google Play Console的盈利设置部分说明这个主题。有关包含截图的详细指南,请参阅Adapty文档

服务器通知:

{
  "message": {
    "data": "eyJ2ZXJzaW9uIjoiMS4wIiwicGFja2FnZU5hbWUiOiJjb20uYWRhcHR5LnNhbXBsZV9hcHAiLCJldmVudFRpbWVNaWxsaXMiOiIxNjMwNTI5Mzk3MTI1Iiwic3Vic2NyaXB0aW9uTm90aWZpY2F0aW9uIjp7InZlcnNpb24iOiIxLjAiLCJub3RpZmljYXRpb25UeXBlIjo2LCJwdXJjaGFzZVRva2VuIjoiY2o3anAuQU8tSjFPelIxMjMiLCJzdWJzY3JpcHRpb25JZCI6ImNvbS5hZGFwdHkuc2FtcGxlX2FwcC53ZWVrbHlfc3ViIn19",
    "messageId": "2829603729517390",
    "message_id": "2829603729517390",
    "publishTime": "2021-09-01T20:49:59.124Z",
    "publish_time": "2021-08-04T20:49:59.124Z"
  },
  "subscription": "projects/935083/subscriptions/adapty-rtdn"
}

我们主要关心包含用base64编码的交易数据的数据键。messageId键可以用于消息重复删除,这样您就无需处理重复的消息了。 

下面是服务器通知中的一个交易:

{
  "version": "1.0",
  "packageName": "com.adapty.sample_app",
  "eventTimeMillis": "1630529397125",
  "subscriptionNotification": {
    "version": "1.0",
    "notificationType": 6,
    "purchaseToken": "cj7jp.AO-J1OzR123",
    "subscriptionId": "com.adapty.sample_app.weekly_sub"
  }
}

packageName键可以帮助您了解此事件属于哪个应用程序。subscriptionId键会告诉您涉及哪个订阅,而purchaseToken可以帮您找到特定的交易。有了订阅,您就始终都能在续订链中找到最后一个交易,因为此事件将属于该交易。notificationType键包含事件类型。在我看来,这些是最方便订阅的:

  • (2)SUBSCRIPTION_RENEWED:订阅已续订成功。
  • (3)SUBSCRIPTION_CANCELED:用户已禁用订阅自动续订功能。如果禁用了自动续订,则需要尝试将用户恢复为活跃订阅者。
  • (5)SUBSCRIPTION_ON_HOLD(6)SUBSCRIPTION_IN_GRACE_PERIOD:由于支付问题,订阅无法续订。您应该通知用户,这样他们的订阅才不会被自动取消。
  • (12)SUBSCRIPTION_REVOKED:订阅已被撤销。这意味着用户将失去对订阅服务之前授予的高级功能的访问权。 

在产品(不是订阅)中,您会收到oneTimeProductNotification,而不是subscriptionNotification键。它还将包含sku键而不是subscriptionId键。此外,您只会收到2种产品事件类型

  • (1)ONE_TIME_PRODUCT_PURCHASED:成功的产品购买。
  • (2)ONE_TIME_PRODUCT_CANCELED:产品购买被取消,因为用户还没有付款。

结论

服务器端验证会增加您为应用收集的分析。这让欺诈者更难获取优质内容,并可用于实现跨平台订阅。但是,服务器端验证可能需要相当长的时间来实现,尤其是在必须要有高度数据准确性的情况下。为了提供高质量数据,您需要考虑大量的附加情况,如订阅升级、订阅跨级、试用期、优惠推广(promo offer)和试销优惠价(intro offer)、宽限期(grace period)、退款等。您还必须知道并解释所有的政策细节,比如Google对续订一年以上的订阅只收取15%(而不是30%)的佣金。

Further reading

Adapty December update
Adapty December update
January 11, 2021
5 min read
What's wrong with my in-app subscription on iOS?
What's wrong with my in-app subscription on iOS?
November 18, 2019
8 min read
Adapty API outage and what we've learned from it
Adapty API outage and what we've learned from it
April 8, 2021
5 min read