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

所谓服务器端验证回调,指的是 Google 发送给外部系统的网址请求,其中带有 Google 扩展的查询参数,用来通知外部系统某位用户因为与激励广告或插页式激励广告互动而应予以奖励。激励广告 SSV(服务器端验证)回调提供了额外的保护层,可规避通过欺骗客户端回调来奖励用户的行为。

本指南介绍如何使用 Tink Java Apps第三方 加密库来验证激励广告 SSV 回调,以确保回调中的查询参数均属 合法值。 虽然本指南在介绍时使用的是 Tink,但您可以选择使用任何支持 ECDSA的第三方库。 您还可以在 AdMob 界面中使用 测试 工具 对您的服务器进行测试。

查看使用 Java Spring-boot 的激励广告 SSV 示例

前提条件

使用 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 setCustomData提供的自定义数据字符串。

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

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

如果应用未提供用户标识符,则 SSV 回调中不会显示此查询参数。

1234567

广告来源标识符

广告来源名称和 ID

广告来源名称 广告来源 ID
Ad Generation(出价)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
疾风骤雨3376427960656545613
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 选项:

Java

RewardedAd.load(MainActivity.this, "AD_UNIT_ID",
    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, "AD_UNIT_ID",
    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 提供的公钥。

您可以从 AdMob 密钥 服务器提取用于验证激励广告 SSV 回调的公钥列表。公钥列表以 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"));

从回调网址中获取 signature 和 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 回调,最多尝试 5 次。
如何验证 SSV 回调是否来自 Google?
使用 DNS 反向查找来验证 SSV 回调是否来自 Google。