验证服务器端验证 (SSV) 回调

服务器端验证回调是包含查询参数的网址请求 这些请求会由 Google 发送到外部系统 通知它用户应该因与激励广告互动而获得奖励,或 插页式激励广告。激励广告 SSV(服务器端验证)回调 可针对客户端回调的仿冒提供额外一层保护 来奖励用户

本指南介绍了如何使用 Tink Java Apps 第三方 加密库,以确保回调中的查询参数 合理价值。 虽然本指南在介绍时使用的是 Tink,但您可以选择 使用任何支持 ECDSA。 您也可以通过测试 工具

查看此功能完全正常运行 示例 使用 Java Spring-boot

前提条件

使用 Tink Java Apps 库中的 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 值相对应的名称列在广告 来源标识符部分。 1953547073528090325
ad_unit 用于请求激励广告的 AdMob 广告单元 ID。 2747237135
custom_data 自定义数据字符串,由 <ph type="x-smartling-placeholder"></ph> setCustomData

如果应用未提供自定义数据字符串,则此查询参数 值将不会出现在 SSV 回调中。

SAMPLE_CUSTOM_DATA_STRING
key_id 用于验证 SSV 回调的密钥。此值映射到一个公钥 AdMob 密钥服务器提供的密钥。 1234567890
reward_amount 广告单元设置中指定的奖励金额。 5
reward_item 广告单元设置中指定的奖品。 金币
signature AdMob 生成的 SSV 回调的签名。 MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY
timestamp 用户获奖时间戳(以毫秒为单位的 Epoch 时间)。 1507770365237823
transaction_id AdMob 为每个奖励授予事件生成的唯一的十六进制编码标识符。 18fa792de1bca816048293fc71035638
user_id 用户标识符 setUserId

如果应用未提供用户标识符,此查询参数将不会 出现在 SSV 回调中。

1234567

广告来源标识符

广告来源名称和 ID

Ad source name Ad source ID
Aarki (bidding)5240798063227064260
Ad Generation (bidding)1477265452970951479
AdColony15586990674969969776
AdColony (non-SDK) (bidding)4600416542059544716
AdColony (bidding)6895345910719072481
AdFalcon3528208921554210682
AdMob Network5450213213286189855
AdMob Network Waterfall1215381445328257950
ADResult10593873382626181482
AMoAd17253994435944008978
Applovin1063618907739174004
Applovin (bidding)1328079684332308356
Chartboost2873236629771172317
Chocolate Platform (bidding)6432849193975106527
CrossChannel (MdotM)9372067028804390441
Custom Event18351550913290782395
DT Exchange*
* Prior to September 21, 2022, this network was called "Fyber Marketplace".
2179455223494392917
EMX (bidding)8497809869790333482
Fluct (bidding)8419777862490735710
Flurry3376427960656545613
Fyber*
* This ad source is used for historical reporting.
4839637394546996422
i-mobile5208827440166355534
Improve Digital (bidding)159382223051638006
Index Exchange (bidding)4100650709078789802
InMobi7681903010231960328
InMobi (bidding)6325663098072678541
InMobi Exchange (bidding)5264320421916134407
IronSource6925240245545091930
ironSource Ads (bidding)1643326773739866623
Leadbolt2899150749497968595
LG U+AD18298738678491729107
LINE Ads Network3025503711505004547
maio7505118203095108657
maio (bidding)1343336733822567166
Media.net (bidding)2127936450554446159
Mediated house ads6060308706800320801
Meta Audience Network*
* Prior to June 6, 2022, this network was called "Facebook Audience Network".
10568273599589928883
Meta Audience Network (bidding)*
* Prior to June 6, 2022, this network was called "Facebook Audience Network (bidding)".
11198165126854996598
Mintegral1357746574408896200
Mintegral (bidding)6250601289653372374
MobFox8079529624516381459
MobFox (bidding)3086513548163922365
MoPub (deprecated)10872986198578383917
myTarget8450873672465271579
Nend9383070032774777750
Nexxen (bidding)*

* Prior to May 1, 2024, this network was called "UnrulyX".

2831998725945605450
ONE by AOL (Millennial Media)6101072188699264581
ONE by AOL (Nexage)3224789793037044399
OneTag Exchange (bidding)4873891452523427499
OpenX (bidding)4918705482605678398
Pangle4069896914521993236
Pangle (bidding)3525379893916449117
PubMatic (bidding)3841544486172445473
Reservation campaign7068401028668408324
RhythmOne (bidding)2831998725945605450
Rubicon (bidding)3993193775968767067
SK planet734341340207269415
Sharethrough (bidding)5247944089976324188
Smaato (bidding)3362360112145450544
Equativ (bidding)*

* Prior to January 12, 2023, this network was called "Smart Adserver".

5970199210771591442
Sonobi (bidding)3270984106996027150
Tapjoy7295217276740746030
Tapjoy (bidding)4692500501762622178
Tencent GDT7007906637038700218
TripleLift (bidding)8332676245392738510
Unity Ads4970775877303683148
Unity Ads (bidding)7069338991535737586
Verizon Media7360851262951344112
Verve Group (bidding)5013176581647059185
Vpon1940957084538325905
Liftoff Monetize*

* Prior to January 30, 2023, this network was called "Vungle".

1953547073528090325
Liftoff Monetize (bidding)*

* Prior to January 30, 2023, this network was called "Vungle (bidding)".

4692500501762622185
Yieldmo (bidding)4193081836471107579
YieldOne (bidding)3154533971590234104
Zucks5506531810221735863

奖励用户

在决定采用哪种方法时,务必要在用户体验和奖励验证之间取得平衡 何时奖励用户服务器端回调 访问外部系统。因此,建议的最佳做法是使用 客户端回调以立即奖励用户,同时执行 在收到服务器端回调时对所有奖励进行验证。这个 方法不仅能提供良好的用户体验,还能确保 奖励。

不过,对于对奖励有效性至关重要的应用(例如, 奖励会影响应用的游戏内经济),且奖励延迟会造成延迟 可接受,等待经过验证的服务器端回调可能是最佳选择 方法。

自定义数据

对于需要服务器端验证回调中额外数据的应用,应使用 激励广告的自定义数据功能。在激励广告上设置的任何字符串值 对象会传递给 SSV 回调的 custom_data 查询参数。如果拒绝 自定义数据值后,custom_data 查询参数值不会 。

以下代码示例演示了如何在 激励广告。

Java

RewardedAd.load(MainActivity.this, "ca-app-pub-3940256099942544/5354046379",
    new AdRequest.Builder().build(),  new RewardedAdLoadCallback() {
  @Override
  public void onAdLoaded(RewardedAd ad) {
    Log.d(TAG, "Ad was loaded.");
    rewardedAd = ad;
    ServerSideVerificationOptions options = new ServerSideVerificationOptions
        .Builder()
        .setCustomData("SAMPLE_CUSTOM_DATA_STRING")
        .build();
    rewardedAd.setServerSideVerificationOptions(options);
  }
  @Override
  public void onAdFailedToLoad(LoadAdError loadAdError) {
    Log.d(TAG, loadAdError.toString());
    rewardedAd = null;
  }
});

Kotlin

RewardedAd.load(this, "ca-app-pub-3940256099942544/5354046379",
    AdRequest.Builder().build(), object : RewardedAdLoadCallback() {
  override fun onAdLoaded(ad: RewardedAd) {
    Log.d(TAG, "Ad was loaded.")
    rewardedInterstitialAd = ad
    val options = ServerSideVerificationOptions.Builder()
        .setCustomData("SAMPLE_CUSTOM_DATA_STRING")
        .build()
    rewardedAd.setServerSideVerificationOptions(options)
  }

  override fun onAdFailedToLoad(adError: LoadAdError) {
    Log.d(TAG, adError?.toString())
    rewardedAd = null
  }
})

如果您要设置自定义奖励字符串,则必须在展示之前设置 。

手动验证激励广告 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 会在 为 1 秒。
如何验证 SSV 回调是否来自 Google?
使用 DNS 反向查找来验证 SSV 回调是否来自 Google。