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

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

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

查看使用 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 自定义数据字符串,提供方: 。

如果应用未提供自定义数据字符串,此查询参数值将不会出现在 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
时间戳 用户获奖时间戳(以毫秒为单位的纪元时间)。 1507770365237823
transaction_id AdMob 为每个奖励授予事件生成的唯一的十六进制编码标识符。 18fa792de1bca816048293fc71035638
user_id 用户标识符,由 SetUserId

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

1234567

广告来源标识符

广告来源名称和 ID

广告来源名称 广告来源 ID
Aarki(出价)5240798063227064260
生成广告(出价)1477265452970951479
AdColony15586990674969969776
AdColony(非 SDK)(出价)4600416542059544716
AdColony(出价)6895345910719072481
AdFalcon3528208921554210682
AdMob 广告联盟5450213213286189855
ADResult10593873382626181482
AMoAd17253994435944008978
AppLovin1063618907739174004
AppLovin(出价)1328079684332308356
Chartboost2873236629771172317
Chocolate Platform(出价)6432849193975106527
跨渠道 (MdotM)9372067028804390441
自定义事件18351550913290782395
DT Exchange*
* 在 2022 年 9 月 21 日之前,该广告联盟称为“Fyber Marketplace”。
2179455223494392917
EMX(出价)8497809869790333482
Fluct(出价)8419777862490735710
小雪3376427960656545613
Fyber*
* 此广告来源用于生成历史报告。
4839637394546996422
i-mobile5208827440166355534
改进数字(出价)159382223051638006
Index Exchange(出价)4100650709078789802
InMobi7681903010231960328
InMobi(出价)6325663098072678541
IronSource6925240245545091930
Leadbolt2899150749497968595
LG U+AD18298738678491729107
LINE 广告网络3025503711505004547
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
MobFox8079529624516381459
MobFox(出价)3086513548163922365
MoPub(已弃用10872986198578383917
myTarget8450873672465271579
Nend9383070032774777750
AOL (Millennial Media) 的 ONE6101072188699264581
AOL 提供的 ONE (Nexage)3224789793037044399
OneTag Exchange(出价)4873891452523427499
OpenX(出价)4918705482605678398
Pangle(出价)3525379893916449117
PubMatic(出价)3841544486172445473
预订型广告系列7068401028668408324
RhythmOne(出价)2831998725945605450
Rubicon(出价)3993193775968767067
SK 星球734341340207269415
Sharethrough(出价)5247944089976324188
Smaato(出价)3362360112145450544
Equativ(出价)*

* 在 2023 年 1 月 12 日之前,该广告联盟称为“Smart Adserver”。

5970199210771591442
Sonobi(出价)3270984106996027150
Tapjoy7295217276740746030
Tapjoy(出价)4692500501762622178
Tencent GDT7007906637038700218
TripleLift(出价)8332676245392738510
Unity 广告4970775877303683148
UnrulyX(出价)2831998725945605450
Verizon Media7360851262951344112
Verve Group(出价)5013176581647059185
Vpon1940957084538325905
Liftoff Monetize*

* 在 2023 年 1 月 30 日之前,该广告联盟称为“Vungle”。

1953547073528090325
Liftoff Monetize(出价)*

* 在 2023 年 1 月 30 日之前,该广告联盟称为“Vungle(出价)”。

4692500501762622185
Yieldmo(出价)4193081836471107579
YieldOne(出价)3154533971590234104
Zucks5506531810221735863

奖励用户

在决定何时奖励用户时,请务必在用户体验和奖励验证之间取得平衡。服务器端回调在到达外部系统之前可能会遇到延迟。因此,建议的最佳做法是使用客户端回调立即奖励用户,同时在收到服务器端回调时对所有奖励执行验证。这种方法既能提供良好的用户体验,又能确保所获奖励的有效性。

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

自定义数据

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

以下代码示例演示了如何在加载激励广告后设置 SSV 选项。

void HandleRewardedAdLoaded(RewardedAd ad, AdFailedToLoadEventArgs error)
{
    // Create and pass the SSV options to the rewarded ad.
    var options = new ServerSideVerificationOptions
                          .Builder()
                          .SetCustomData("SAMPLE_CUSTOM_DATA_STRING")
                          .Build()
    ad.SetServerSideVerificationOptions(options);
}

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

手动验证激励广告 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);
  }
}

如果方法执行顺利,未发生任何异常,则表示回调网址已验证成功。

FAQ

我可以缓存 AdMob 密钥服务器提供的公钥吗?
我们建议您缓存 AdMob 密钥服务器提供的公钥,以减少验证 SSV 回调所需的操作数量。但请注意,公钥会经常轮替,因此缓存时间不应超过 24 小时。
AdMob 密钥服务器提供的公钥的轮替频率是多少?
AdMob 密钥服务器提供的公钥会不定期轮替。为确保继续按预期验证 SSV 回调,公钥的缓存时间不应超过 24 小时。
如果无法访问我的服务器,会怎么样?
Google 预计您的服务器会针对 SSV 回调返回 HTTP 200 OK 成功状态响应代码。如果您的服务器无法访问或未提供预期的响应,Google 将重新尝试发送 SSV 回调,每隔 1 秒发送最多 5 次。
如何验证 SSV 回调是否来自 Google?
使用 DNS 反向查找来验证 SSV 回调是否来自 Google。