# 应用程序内购买验证

**URL:** https://heroiclabs.com/docs/zh/nakama/concepts/iap-validation/
**Summary:** 应用程序内购买使得玩家可以通过购买，获得解锁、游戏内消耗品、订阅等等。必须对购买进行验证，让玩家始终能体验到诚信和安全性。Nakama支持Apple、Google和华为服务的单个产品和订阅购买验证。

---


# 应用程序内购买验证

货币化模式和工具非常广泛，包括广告支持、微交易、免费增值、一次性购买以及其间的一切。许多这类解决方案中的一个关键工具是应用程序内购买 (IAP)。借助 IAP，可进行单笔购买，获得解锁、游戏内消费品、会员访问权限订阅等等。

对于最常见的应用程序内购买实现，存在许多现成攻击。其关注点通常包括：

- 向客户馈送虚假的购买响应，以示成功
- 多次重播某个有效的购买响应
- 与另外一个客户端共享购买响应，让多个玩家能够得到单笔购买的回报

对于应用程序内购买，需要有可信的真相来源。Nakama 检查和跟踪购买和购买历史，解决了很多可能的漏洞和难点：

{{< table name="nakama.concepts.iap-validation.pain-points" >}}

无论在哪个平台上，均可以对 [Apple](#apple)、[Google](#google) 和[华为](#huawei)购买使用应用程序内购买验证。单个产品和订阅购买均受支持。

通过 [Unity IAP](#unity-iap) 进行的 Apple 和 Google 购买也受支持。

## Apple

Nakama 支持在 iOS 中进行的产品和订阅购买的验证。

Apple 购买收据将被发送到 Apple 进行验证。按照 Apple 的建议，同时使用生产服务器和沙盒服务器来验证收据。

### 设置

为对照 App Store 验证收据，Nakama 需要您应用程序的共享密钥。

1. 在您应用程序的应用程序内购买管理部分下的 [App Store Connect](https://appstoreconnect.apple.com) 中设置共享密钥：
![Apple App Store Connect]({{< fingerprint_image "/images/pages/nakama/concepts/iap-validation/apple_iap_1.png" >}})

2. 记录共享密钥，以便在 Nakama 配置中使用：
![Apple App Store Connect 共享密钥]({{< fingerprint_image "/images/pages/nakama/concepts/iap-validation/apple_iap_2.png" >}})

3. 将 Nakama 的`iap.apple.shared_password` [配置标志](../../getting-started/configuration/#apple)的值设置为先前创建的共享密钥的值。

### 经过验证的购买

Nakama 仅支持验证 iOS 7+ 收据。

Apple 收据可包含多笔购买，Nakama 将进行全部验证并将其作为单独的购买记录存储。

{{< code type="client" >}}
```bash
curl "http://127.0.0.1:7350/v2/iap/purchase/apple \
  --user 'defaultkey:' \
  --data '{"receipt":"base64_encoded_receipt_data"}'
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
local function validate_receipt(receipt)
  local result = client.validate_purchase_apple(receipt)
  if result.error then
    print(result.message)
    return
  end
  pprint(result)
end

-- Use https://defold.com/extension-iap/
iap.set_listener(function(self, transaction, error)
  if not error then
    validate_receipt(transaction.receipt)
  end
end)
iap.buy("com.defold.nakama.goldbars-10")
```
{{< / code >}}

{{< code type="client" >}}
```csharp
string appleReceipt = "<receipt>";
var response = await client.ValidatePurchaseAppleAsync(session, appleReceipt);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const appleReceipt = "<receipt>";
const result = await client.validatePurchaseApple(session, appleReceipt);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var apple_receipt = "<receipt>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_apple_async(session, apple_receipt), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
```
{{< / code >}}

{{< code type="client" >}}
```shell
POST /v2/iap/purchase/apple
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "receipt": "base64_encoded_Apple_receipt_payload"
}
```
{{< / code >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="java" />}}

请参考提供的运行时[购买验证功能](../../server-framework/typescript-runtime/function-reference/#purchaseValidateApple)的功能参考页面。

## Google

Nakama 支持在 Android 中进行的产品和订阅购买的验证。

### 设置

为对照 Play Store 验证收据，Nakama 需要您的 Google 服务帐户 `ClientEmail` 和 `PrivateKey`。获取这些值：

1. 在 [Google API Console](https://play.google.com/console/developer/) 中设置[服务帐户](https://developers.google.com/android-publisher/getting_started#using_a_service_account)：
![创建服务帐户]({{< fingerprint_image "/images/pages/nakama/concepts/iap-validation/google_iap_1_create_service_account.jpg" >}})

2. 创建服务帐户后，需要创建密钥：
![创建密钥]({{< fingerprint_image "/images/pages/nakama/concepts/iap-validation/google_iap_2_create_key.jpg" >}})

3. 下载 JSON 文件形式的密钥：
![创建 JSON 密钥]({{< fingerprint_image "/images/pages/nakama/concepts/iap-validation/google_iap_3_create_key_2.jpg" >}})

4. 打开 JSON 文件，提取 `ClientEmail` 和 `PrivateKey` 的值。

5. 将这些值分别设置为 `iap.google.client_email` 和 `iap.google.private_key` 的 [Nakama 配置](../../getting-started/configuration/#iap-in-app-purchase)值。

6. 最后，确保向购买验证 API 授予 Nakama 访问权限。从 [Google Play Developer Console](https://play.google.com/apps/publish)，依次进入**设置** > **API 访问权限**：
![创建 API 访问权限]({{< fingerprint_image "/images/pages/nakama/concepts/iap-validation/google_iap_5_play_api.jpg" >}})

7. 您在先前的步骤中创建的服务帐户应在上方列出。授予服务帐户访问 API 的访问权限，确保授予服务帐户对**可见性**、**查看财务数据**和**管理订单**的访问权限。Nakama 对照 Google Play 验证收据需要这些权限。
![授予访问权限]({{< fingerprint_image "/images/pages/nakama/concepts/iap-validation/google_iap_6_grant_access.jpg" >}})

8. 进入**用户和权限**，检查正确设定了服务帐户：
![列出有访问权限的用户]({{< fingerprint_image "/images/pages/nakama/concepts/iap-validation/google_iap_7_play_users.jpg" >}})

### 验证购买

{{< note "important" >}}
期望的 Google 收据是 `Purchase.getOriginalJson()` 返回的字符串。请参阅 [Google 开发者文档](https://developer.android.com/reference/com/android/billingclient/api/Purchase#getOriginalJson())，了解详情。
{{< / note >}}

{{< code type="client" >}}
```bash
curl "http://127.0.0.1:7350/v2/iap/purchase/google \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data"}'
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
local function validate_receipt(receipt)
  local result = client.validate_purchase_google(receipt)
  if result.error then
    print(result.message)
    return
  end
  pprint(result)
end

-- Use https://defold.com/extension-iap/
iap.set_listener(function(self, transaction, error)
  if not error then
    validate_receipt(transaction.receipt)
  end
end)
iap.buy("com.defold.nakama.goldbars-10")
```
{{< / code >}}

{{< code type="client" >}}
```csharp
string googleReceipt = "<receipt>";
var response = await client.ValidatePurchaseGoogleAsync(session, googleReceipt);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const googleReceipt = "<receipt>";
const result = await client.validatePurchaseGoogle(session, googleReceipt);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var google_receipt = "<receipt>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_google_async(session, google_receipt), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
```
{{< / code >}}

{{< code type="client" >}}
```shell
POST /v2/iap/purchase/google
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data"
}
```
{{< / code >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="java" />}}

请参考提供的运行时[购买验证功能](../../server-framework/typescript-runtime/function-reference/#purchaseValidateGoogle)的功能参考页面。

## 华为

Nakama 对照其 IAP 验证服务，验证华为购买。按照华为的建议，对照提供的签名检查购买数据的有效性后，才与华为服务联系。如数据因任何原因而无效，将在通过华为验证服务进行验证之前拒绝购买。

### 验证购买

{{< code type="client" >}}
```bash
curl "http://127.0.0.1:7350/v2/iap/purchase/huawei \
  --user 'defaultkey:' \
  --data '{"purchase":"json_encoded_purchase_data","signature":"purchase_data_signature"}'
```
{{< / code >}}

{{< code type="client" framework="defold" >}}
```lua
-- Huawei purchases are not yet supported by https://defold.com/extension-iap/
```
{{< / code >}}

{{< code type="client" >}}
```csharp
string huaweiReceipt = "<receipt>";
string huaweiSignature = "<signature>";
var response = await client.ValidatePurchaseHuaweiAsync(session, huaweiReceipt, huaweiSignature);
foreach (var validatedPurchase in response.ValidatedPurchases)
{
    System.Console.WriteLine("Validated purchase: " + validatedPurchase);
}
```
{{< / code >}}

{{< code type="client" >}}
```javascript
const huaweiReceipt = "<receipt>";
const huaweiSignature = "<signature>";
const result = await client.validatePurchaseHuawei(session, huaweiReceipt, huaweiSignature);
result.validatedPurchases.forEach(validatedPurchase => {
  console.info("Validated purchase:", validatedPurchase)
});
```
{{< / code >}}

{{< code type="client" framework="godot3" >}}
```gdscript
var huawei_receipt = "<receipt>"
var huawei_signature = "<signature>"
var result : NakamaAPI.ApiValidatePurchaseResponse = yield(client.validate_purchase_huawei_async(session, huawei_receipt, huawei_signature), "completed")
if result.is_exception():
    print("An error occurred: %s" % result)
    return
for p in result.validated_purchase:
    print("Validated purchase: %s" % p.validated_purchase)
```
{{< / code >}}

{{< code type="client" >}}
```shell
POST /v2/iap/purchase/huawei
Host: 127.0.0.1:7350
Accept: application/json
Content-Type: application/json
Authorization: Bearer <session token>
{
  "purchase":"json_encoded_purchase_data",
  "signature":"purchase_data_signature"
}
```
{{< / code >}}

{{< missing type="client" lang="cpp" />}}
{{< missing type="client" lang="java" />}}

请参考提供的运行时[购买验证功能](../../server-framework/typescript-runtime/function-reference/#purchaseValidateHuawei)的功能参考页面。

## Unity IAP

[Unity IAP](https://docs.unity3d.com/Packages/com.unity.purchasing@4.1/manual/index.html) 受到支持，可与大多数流行的应用商店集成，包括 Apple 和 Google。

Unity IAP 购买收据包含以下信息：

{{< table name="nakama.concepts.iap-validation.unity-receipts" >}}

### 验证购买

{{< missing type="server" lang="lua" />}}
{{< missing type="server" lang="typescript" />}}

{{< code type="server" >}}
```go
type unityIAP struct {
	  Payload       string `json:"Payload"`
	  Store         string `json:"Store"`
	  TransactionID string `json:"TransactionID"`
}

switch unityIAP.Store {
    case "GooglePlay":
        validatedReceipt, err = nk.PurchaseValidateGoogle(ctx, userID, wrapper.Payload)
    case "AppleAppStore":
        validatedReceipt, err = nk.PurchaseValidateApple(ctx, userID, wrapper.Payload)
    default:
        logger.Warn("Unrecognised store type in Unity IAP.")
      return ErrBadInput
    }

    if err != nil {
        if err == runtime.ErrPurchaseReceiptAlreadySeen {
            logger.WithField("err", err).Warn("Receipt replay attack.")
        } else {
            logger.WithField("err", err).Error("Receipt validation error.")
        }
        return ErrBadInput
}
```
{{< / code >}}

## 解释验证结果

验证结果中会列出经过验证的购买。

因为 Apple 可能会将多笔购买加入单一收据中，因此结果列表中每次经过验证的购买将仅包含经过验证的购买，以及一个表明其是否已经出现过的 `boolean` 值。如果某笔购买先前已获得 Nakama 验证，“是否出现过”值将是 `true`，这样开发者即可区分新的购买，防止重播攻击的发生。

对于 Google 和华为，每次验证与单笔购买对应。单笔购买包含在验证响应列表中，也有一个上述“是否出现过”值。

每次经过验证的购买都包含提供商验证响应的有效负载，以备开发者出于任何原因需要它。

如果购买/收据无效，验证因为任何原因失败，或者无法与提供商联系，将返回错误。


[apple_iap_1]: images/apple_iap_1.jpg "Apple App Store Connect"
[apple_iap_2]: images/apple_iap_2.jpg "Apple App Store Connect 共享密钥"
[google_iap_1_create_service_account]: images/google_iap_1_create_service_account.jpg "创建服务帐户"
[google_iap_2_create_key]: images/google_iap_2_create_key.jpg "创建密钥"
[google_iap_3_create_key_2]: images/google_iap_3_create_key_2.jpg "创建密钥"
[google_iap_4_create_key_3]: images/google_iap_4_create_key_3.jpg "创建密钥"
[google_iap_5_play_api]: images/google_iap_5_play_api.jpg "创建 API 访问权限"
[google_iap_6_grant_access]: images/google_iap_6_grant_access.jpg "授予访问权限"
[google_iap_7_play_users]: images/google_iap_7_play_users.jpg "列出拥有访问权限的用户"
