驗證伺服器端驗證 (SSV) 回呼

伺服器端驗證回呼是網址要求,其中包含 Google 擴充的查詢參數,Google 會將這類要求傳送至外部系統,通知系統使用者應因與獎勵或獎勵插頁式廣告互動而獲得獎勵。獎勵伺服器端驗證 (SSV) 回呼可提供額外防護,防止用戶端回呼遭冒用,以獎勵使用者。

本指南說明如何使用 Tink Java 應用程式第三方加密程式庫,驗證 SSV 回呼中的查詢參數是否為合法值,確保回呼的有效性。雖然本指南使用 Tink,但您可以選擇使用任何支援 ECDSA 的第三方程式庫。您也可以使用 AdMob 使用者介面的測試工具測試伺服器。

必要條件

使用 Tink Java 應用程式程式庫中的 RewardedAdsVerifier

Tink Java Apps GitHub 存放區包含 RewardedAdsVerifier 輔助類別,可減少驗證已獲得獎勵的 SSV 回呼所需的程式碼。使用這個類別,您就能透過下列程式碼驗證回呼網址。

RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
    .fetchVerifyingPublicKeysWith(
        RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
    .build();
String rewardUrl = ...;
verifier.verify(rewardUrl);

如果 verify() 方法執行時未引發例外狀況,表示回呼網址已成功驗證。「獎勵使用者」一節詳細說明獎勵使用者的最佳做法。如要瞭解這個類別執行的步驟,以驗證獎勵 SSV 回呼,請參閱「手動驗證獎勵 SSV」一節。

SSV 回呼參數

伺服器端驗證回呼包含描述獎勵廣告互動的查詢參數。參數名稱、說明和範例值如下所示。系統會依字母順序傳送參數。

參數名稱 說明 範例值
ad_network 為這個廣告提供廣告的廣告來源 ID。與 ID 值對應的廣告來源名稱會列在「廣告來源 ID」部分。 1953547073528090325
ad_unit 用於請求獎勵廣告的 AdMob 廣告單元 ID。 2747237135
custom_data customRewardString提供的自訂資料字串。

如果應用程式未提供任何自訂資料字串,SSV 回呼中就不會出現這個查詢參數值。

SAMPLE_CUSTOM_DATA_STRING
key_id 用於驗證 SSV 回呼的金鑰。這個值會對應至 AdMob 金鑰伺服器提供的公開金鑰。 1234567890
reward_amount 廣告單元設定中指定的獎勵金額。 5
reward_item 廣告單元設定中指定的獎勵項目。 金幣
簽名 AdMob 產生的 SSV 回呼簽章。 MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY
timestamp 使用者獲得獎勵的時間戳記,以 Epoch 時間表示 (毫秒為單位)。 1507770365237823
transaction_id AdMob 產生的每個獎勵授予事件的專屬十六進位編碼 ID。 18fa792de1bca816048293fc71035638
user_id userIdentifier提供的使用者 ID。

如果應用程式未提供任何使用者 ID,SSV 回呼中就不會出現這個查詢參數。

1234567

廣告來源 ID

廣告來源名稱和 ID

廣告來源名稱 廣告來源 ID
Ad Generation (bidding)1477265452970951479
AdColony15586990674969969776
AdColony (出價)6895345910719072481
AdFalcon3528208921554210682
AdMob 聯播網5450213213286189855
AdMob 聯播網刊登序列1215381445328257950
Applovin1063618907739174004
Applovin (出價)1328079684332308356
Chartboost2873236629771172317
Chocolate Platform (出價)6432849193975106527
自訂事件18351550913290782395
DT Exchange*
* 2022 年 9 月 21 日前,這個聯播網稱為「Fyber Marketplace」。
2179455223494392917
Equativ (出價)*

* 2023 年 1 月 12 日前,這個聯播網稱為「Smart Adserver」。

5970199210771591442
Fluct (出價)8419777862490735710
Flurry3376427960656545613
Fyber*
* 這個廣告來源用於歷來資料報表。
4839637394546996422
i-mobile5208827440166355534
Improve Digital (出價)159382223051638006
Index Exchange (出價)4100650709078789802
InMobi7681903010231960328
InMobi (出價)6325663098072678541
InMobi Exchange (出價)5264320421916134407
IronSource6925240245545091930
ironSource Ads (出價)1643326773739866623
Leadbolt2899150749497968595
Liftoff Monetize*

* 2023 年 1 月 30 日前,這個廣告聯盟稱為「Vungle」。

1953547073528090325
Liftoff Monetize (出價)*

* 2023 年 1 月 30 日前,這個聯播網稱為「Vungle (出價)」。

4692500501762622185
LG U+AD18298738678491729107
LINE Ads Network3025503711505004547
Magnite DV+ (出價)3993193775968767067
maio7505118203095108657
maio (出價)1343336733822567166
Media.net (出價)2127936450554446159
中介服務內部廣告6060308706800320801
Meta Audience Network*
* 2022 年 6 月 6 日前,這個聯播網稱為「Facebook Audience Network」。
10568273599589928883
Meta Audience Network (出價)*
* 2022 年 6 月 6 日前,這個聯播網稱為「Facebook Audience Network (出價)」。
11198165126854996598
Mintegral1357746574408896200
Mintegral (出價)6250601289653372374
MobFox (出價)3086513548163922365
MoPub (已淘汰)10872986198578383917
myTarget8450873672465271579
Nend9383070032774777750
Nexxen (出價)*

* 2024 年 5 月 1 日前,這個聯播網稱為「UnrulyX」。

2831998725945605450
OneTag Exchange (出價)4873891452523427499
OpenX (出價)4918705482605678398
Pangle4069896914521993236
Pangle (出價)3525379893916449117
PubMatic (出價)3841544486172445473
預訂廣告活動7068401028668408324
SK planet734341340207269415
Sharethrough (出價)5247944089976324188
Smaato (出價)3362360112145450544
Sonobi (出價)3270984106996027150
Tapjoy7295217276740746030
Tapjoy (出價)4692500501762622178
Tencent GDT7007906637038700218
TripleLift (出價)8332676245392738510
Unity Ads4970775877303683148
Unity Ads (出價)7069338991535737586
Verve Group (出價)5013176581647059185
Vpon1940957084538325905
Yieldmo (出價)4193081836471107579
YieldOne (出價)3154533971590234104
Zucks5506531810221735863

獎勵使用者

決定何時要獎勵使用者時,請務必兼顧使用者體驗和獎勵驗證。伺服器端回呼可能需要一段時間,才會傳送至外部系統。因此,建議的最佳做法是使用用戶端回呼立即獎勵使用者,同時在收到伺服器端回呼時,驗證所有獎勵。這種做法可確保授予的獎勵有效,同時提供良好的使用者體驗。

不過,如果獎勵有效性至關重要 (例如獎勵會影響應用程式的遊戲內經濟),且可接受獎勵發放延遲,則等待經過驗證的伺服器端回呼可能是最佳做法。

自訂資料

如果應用程式需要在伺服器端驗證回呼中取得額外資料,請使用獎勵廣告的自訂資料功能。在獎勵廣告物件上設定的任何字串值,都會傳遞至 SSV 回呼的 custom_data 查詢參數。如果未設定任何自訂資料值,SSV 回呼中就不會出現 custom_data 查詢參數值。

以下範例會在載入獎勵廣告後設定 SSV 選項:

Swift

RewardedAd.load(with:"AD_UNIT_ID",
                       request: request,
                       completionHandler: { [self] ad, error in
      if let error != error {
      rewardedAd = ad
      let options = ServerSideVerificationOptions()
      options.customRewardString = "SAMPLE_CUSTOM_DATA_STRING"
      rewardedAd.serverSideVerificationOptions = options
    }
})

Objective-C

GADRequest *request = [GADRequest request];
[GADRewardedAd loadWithAdUnitID:@"AD_UNIT_ID"
                        request:request
              completionHandler:^(GADRewardedAd *ad, NSError *error) {
                if (error) {
                  // Handle Error
                  return;
                }
                self.rewardedAd = ad;
                GADServerSideVerificationOptions *options =
                    [[GADServerSideVerificationOptions alloc] init];
                options.customRewardString = @"SAMPLE_CUSTOM_DATA_STRING";
                ad.serverSideVerificationOptions = options;
              }];

手動驗證 SSV 獎勵

以下列出 RewardedAdsVerifier 類別為驗證已獲獎勵的 SSV 執行的步驟。雖然內含的程式碼片段是以 Java 編寫,並運用 Tink 第三方程式庫,但您可以使用支援 ECDSA 的任何第三方程式庫,以所選語言實作這些步驟。

擷取公開金鑰

如要驗證有獎 SSV 回呼,您需要 AdMob 提供的公開金鑰。

如要驗證有獎 SSV 回呼,可從 AdMob 金鑰伺服器擷取公開金鑰清單。公開金鑰清單會以 JSON 表示法提供,格式類似於下列內容:

{
 "keys": [
    {
      keyId: 1916455855,
      pem: "-----BEGIN PUBLIC KEY-----\nMF...YTPcw==\n-----END PUBLIC KEY-----"
      base64: "MFkwEwYHKoZIzj0CAQYI...ltS4nzc9yjmhgVQOlmSS6unqvN9t8sqajRTPcw=="
    },
    {
      keyId: 3901585526,
      pem: "-----BEGIN PUBLIC KEY-----\nMF...aDUsw==\n-----END PUBLIC KEY-----"
      base64: "MFYwEAYHKoZIzj0CAQYF...4akdWbWDCUrMMGIV27/3/e7UuKSEonjGvaDUsw=="
    },
  ],
}

如要擷取公開金鑰,請連線至 AdMob 金鑰伺服器並下載金鑰。下列程式碼會完成這項工作,並將金鑰的 JSON 表示法儲存至 data 變數。

String url = ...;
NetHttpTransport httpTransport = new NetHttpTransport.Builder().build();
HttpRequest httpRequest =
    httpTransport.createRequestFactory().buildGetRequest(new GenericUrl(url));
HttpResponse httpResponse = httpRequest.execute();
if (httpResponse.getStatusCode() != HttpStatusCodes.STATUS_CODE_OK) {
  throw new IOException("Unexpected status code = " + httpResponse.getStatusCode());
}
String data;
InputStream contentStream = httpResponse.getContent();
try {
  InputStreamReader reader = new InputStreamReader(contentStream, UTF_8);
  data = readerToString(reader);
} finally {
  contentStream.close();
}

請注意,公開金鑰會定期輪替。您會收到電子郵件,瞭解即將進行的輪替。如果您正在快取公開金鑰,收到這封電子郵件後,請更新金鑰。

擷取公開金鑰後,必須剖析金鑰。下方的 parsePublicKeysJson 方法會將 JSON 字串 (例如上述範例) 做為輸入內容,並建立從 key_id 值到公開金鑰的對應,這些金鑰會封裝為 Tink 程式庫的 ECPublicKey 物件。

private static Map<Integer, ECPublicKey> parsePublicKeysJson(String publicKeysJson)
    throws GeneralSecurityException {
  Map<Integer, ECPublicKey> publicKeys = new HashMap<>();
  try {
    JSONArray keys = new JSONObject(publicKeysJson).getJSONArray("keys");
    for (int i = 0; i < keys.length(); i++) {
      JSONObject key = keys.getJSONObject(i);
      publicKeys.put(
          key.getInt("keyId"),
          EllipticCurves.getEcPublicKey(Base64.decode(key.getString("base64"))));
    }
  } catch (JSONException e) {
    throw new GeneralSecurityException("failed to extract trusted signing public keys", e);
  }
  if (publicKeys.isEmpty()) {
    throw new GeneralSecurityException("No trusted keys are available.");
  }
  return publicKeys;
}

取得要驗證的內容

獎勵 SSV 回呼的最後兩個查詢參數一律為 signaturekey_id,,且順序固定。其餘查詢參數則會指定要驗證的內容。假設您已將 AdMob 設為將獎勵回呼傳送至 https://www.myserver.com/mypath。以下程式碼片段顯示 SSV 回呼的範例,並醒目顯示待驗證的內容。

https://www.myserver.com/path?ad_network=54...55&ad_unit=12345678&reward_amount=10&reward_item=coins
&timestamp=150777823&transaction_id=12...DEF&user_id=1234567&signature=ME...Z1c&key_id=1268887

下列程式碼示範如何從回呼網址剖析要驗證的內容,並以 UTF-8 位元組陣列的形式傳回。

public static final String SIGNATURE_PARAM_NAME = "signature=";
...
URI uri;
try {
  uri = new URI(rewardUrl);
} catch (URISyntaxException ex) {
  throw new GeneralSecurityException(ex);
}
String queryString = uri.getQuery();
int i = queryString.indexOf(SIGNATURE_PARAM_NAME);
if (i == -1) {
  throw new GeneralSecurityException("needs a signature query parameter");
}
byte[] queryParamContentData =
    queryString
        .substring(0, i - 1)
        // i - 1 instead of i because of & in the query string
        .getBytes(Charset.forName("UTF-8"));

從回呼網址取得簽章和 key_id

使用上一個步驟的 queryString 值,從回呼網址剖析 signaturekey_id 查詢參數,如下所示:

public static final String KEY_ID_PARAM_NAME = "key_id=";
...
String sigAndKeyId = queryString.substring(i);
i = sigAndKeyId.indexOf(KEY_ID_PARAM_NAME);
if (i == -1) {
  throw new GeneralSecurityException("needs a key_id query parameter");
}
String sig =
    sigAndKeyId.substring(
        SIGNATURE_PARAM_NAME.length(), i - 1 /* i - 1 instead of i because of & */);
int keyId = Integer.valueOf(sigAndKeyId.substring(i + KEY_ID_PARAM_NAME.length()));

執行驗證

最後一個步驟是使用適當的公開金鑰驗證回呼網址的內容。取得 parsePublicKeysJson 方法傳回的對應,並使用回呼網址中的 key_id 參數,從該對應取得公開金鑰。然後使用該公開金鑰驗證簽章。這些步驟會在下方的 verify 方法中示範。

private void verify(final byte[] dataToVerify, int keyId, final byte[] signature)
    throws GeneralSecurityException {
  Map<Integer, ECPublicKey> publicKeys = parsePublicKeysJson();
  if (publicKeys.containsKey(keyId)) {
    foundKeyId = true;
    ECPublicKey publicKey = publicKeys.get(keyId);
    EcdsaVerifyJce verifier = new EcdsaVerifyJce(publicKey, HashType.SHA256, EcdsaEncoding.DER);
    verifier.verify(signature, dataToVerify);
  } else {
    throw new GeneralSecurityException("cannot find verifying key with key ID: " + keyId);
  }
}

如果方法執行時未擲回例外狀況,表示回呼網址已成功驗證。

常見問題

我可以快取 AdMob 金鑰伺服器提供的公開金鑰嗎?
建議您快取 AdMob 金鑰伺服器提供的公開金鑰,減少驗證 SSV 回呼所需的操作次數。不過請注意,公開金鑰會定期輪替,因此快取時間不應超過 24 小時。
AdMob 金鑰伺服器提供的公開金鑰多久會輪替一次?
AdMob 金鑰伺服器提供的公開金鑰會按照可變動的時間表輪替。為確保 SSV 回呼驗證作業能繼續正常運作,公開金鑰的快取時間不應超過 24 小時。
如果無法連上伺服器,會發生什麼情況?
Google 預期 SSV 回呼會傳回 HTTP 200 OK 成功狀態回應代碼。如果無法連上伺服器或伺服器未提供預期回應,Google 會以一秒為間隔,最多重試傳送 SSV 回呼五次。
如何確認 SSV 回呼來自 Google?
使用反向 DNS 查詢,確認 SSV 回呼是否來自 Google。