所谓服务器端验证回调,指的是 Google 发送给外部系统的网址请求,其中带有 Google 扩展的查询参数,用来通知外部系统某位用户因为与激励广告或插页式激励广告互动而应予以奖励。激励广告 SSV(服务器端验证)回调提供了额外的保护层,可规避通过欺骗客户端回调来奖励用户的行为。
本指南介绍如何使用 Tink Java Apps 第三方加密库来验证激励广告 SSV 回调,以确保回调中的查询参数均属合法值。虽然本指南在介绍时使用的是 Tink,但您可以选择使用任何支持 ECDSA 的第三方库。您还可以在 AdMob 界面中使用测试工具对您的服务器进行测试。
前提条件
- 在广告单元中启用激励广告服务器端验证。
使用 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 | 自定义数据字符串,其提供方法为 customData。如果应用未提供自定义数据字符串,此查询参数值将不会出现在 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 | 用户标识符,其提供方法为 userId。如果应用未提供用户标识符,此查询参数将不会出现在 SSV 回调中。 | 1234567 | 
广告来源标识符
广告来源名称和 ID
| 广告来源名称 | 广告来源 ID | 
|---|---|
| Ad Generation(出价) | 1477265452970951479 | 
| AdColony | 15586990674969969776 | 
| AdColony(出价) | 6895345910719072481 | 
| AdFalcon | 3528208921554210682 | 
| AdMob 广告联盟 | 5450213213286189855 | 
| AdMob 广告联盟瀑布流 | 1215381445328257950 | 
| AppLovin | 1063618907739174004 | 
| Applovin(出价) | 1328079684332308356 | 
| Chartboost | 2873236629771172317 | 
| Chocolate Platform(出价) | 6432849193975106527 | 
| 自定义事件 | 18351550913290782395 | 
| DT Exchange* * 在 2022 年 9 月 21 日之前,该广告联盟称为“Fyber Marketplace”。 | 2179455223494392917 | 
| Equativ(出价)* * 在 2023 年 1 月 12 日之前,该广告联盟称为“Smart Adserver”。 | 5970199210771591442 | 
| Fluct(出价) | 8419777862490735710 | 
| Flurry | 3376427960656545613 | 
| Fyber* * 此广告来源用于生成历史报告。 | 4839637394546996422 | 
| i-mobile | 5208827440166355534 | 
| Improve Digital(出价) | 159382223051638006 | 
| Index Exchange(出价) | 4100650709078789802 | 
| InMobi | 7681903010231960328 | 
| InMobi(出价) | 6325663098072678541 | 
| InMobi Exchange(出价) | 5264320421916134407 | 
| IronSource | 6925240245545091930 | 
| ironSource Ads(出价) | 1643326773739866623 | 
| Leadbolt | 2899150749497968595 | 
| Liftoff Monetize* * 在 2023 年 1 月 30 日之前,该广告联盟称为“Vungle”。 | 1953547073528090325 | 
| Liftoff Monetize(出价)* * 在 2023 年 1 月 30 日之前,该广告联盟称为“Vungle(出价)”。 | 4692500501762622185 | 
| LG U+AD | 18298738678491729107 | 
| LINE Ads Network | 3025503711505004547 | 
| Magnite DV+(出价) | 3993193775968767067 | 
| maio | 7505118203095108657 | 
| 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 | 
| Mintegral | 1357746574408896200 | 
| Mintegral(出价) | 6250601289653372374 | 
| MobFox(出价) | 3086513548163922365 | 
| MoPub(已弃用) | 10872986198578383917 | 
| myTarget | 8450873672465271579 | 
| Nend | 9383070032774777750 | 
| Nexxen(出价)* * 在 2024 年 5 月 1 日之前,该广告联盟称为“UnrulyX”。 | 2831998725945605450 | 
| OneTag Exchange(出价) | 4873891452523427499 | 
| OpenX(出价) | 4918705482605678398 | 
| Pangle | 4069896914521993236 | 
| Pangle(出价) | 3525379893916449117 | 
| PubMatic(出价) | 3841544486172445473 | 
| 预订型广告系列 | 7068401028668408324 | 
| SK planet | 734341340207269415 | 
| Sharethrough(出价) | 5247944089976324188 | 
| Smaato(出价) | 3362360112145450544 | 
| Sonobi(出价) | 3270984106996027150 | 
| Tapjoy | 7295217276740746030 | 
| Tapjoy(出价) | 4692500501762622178 | 
| Tencent GDT | 7007906637038700218 | 
| TripleLift(出价) | 8332676245392738510 | 
| Unity Ads | 4970775877303683148 | 
| Unity Ads(出价) | 7069338991535737586 | 
| Verve Group(出价) | 5013176581647059185 | 
| Vpon | 1940957084538325905 | 
| Yieldmo(出价) | 4193081836471107579 | 
| YieldOne(出价) | 3154533971590234104 | 
| Zucks | 5506531810221735863 | 
奖励用户
在决定何时奖励用户时,请务必在用户体验与奖励验证之间取得平衡。服务器端回调在到达外部系统之前,可能会出现延迟。因此,我们建议的最佳做法是通过客户端回调立即奖励用户,同时在收到服务器端回调时对所有奖励进行验证。这种做法可确保奖励符合授予条件,同时提供良好的用户体验。
不过,对于某些应用而言,一方面奖励是否符合授予条件至关重要(例如,奖励会影响应用的游戏内经济效益),另一方面又可接受奖励授予方面的延迟。这时,最佳做法可能是等待服务器端回调完成验证。
自定义数据
对于需要服务器端验证回调中额外数据的应用,应使用激励广告的自定义数据功能。在激励广告对象上设置的任何字符串值都将传递给 SSV 回调的 custom_data 查询参数。如果未设置自定义数据值,custom_data 查询参数值不会出现在 SSV 回调中。
以下示例展示了如何在加载激励广告后设置 SSV 选项:
将 SAMPLE_CUSTOM_DATA_STRING 替换为您的自定义数据。
如果您要设置自定义奖励字符串,则必须在展示广告之前设置。
手动验证激励广告 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 回调的最后两个查询参数始终是 signature 和 key_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 inst&ead of i because of  in the query string
        .getBytes(Charset.forName("UTF-8"));
从回调网址中获取 signature 和 key_id
下列代码通过使用上一步中的 queryString 值,解析回调网址中的 signature 和 key_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 inst&ead 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 回调,每隔 1 秒发送最多 5 次。
- 如何验证 SSV 回调是否来自 Google?
- 使用 DNS 反向查找来验证 SSV 回调是否来自 Google。