分享您对 Google 移动广告 SDK 的反馈!参加年度问卷调查

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

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

本指南介绍如何使用 Tink 第三方加密库来验证激励视频广告 SSV 回调,以确保回调中的查询参数均属合法值。虽然本指南在介绍时使用的是 Tink,但您可以选择使用任何支持 ECDSA 的第三方库。

前提条件

使用 Tink 中的 RewardedAdsVerifier

Tink GitHub 代码库包含 RewardedAdsVerifier 助手类,可减少验证激励视频广告 SSV 回调所需的代码。通过同时使用此类和 Tink 第三方加密库,您就可借助以下代码验证回调网址。

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 广告单元设置中指定的奖品。 金币
signature AdMob 生成的 SSV 回调的签名。 MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY
timestamp 用户获奖时间戳(以毫秒为单位的 Epoch 时间)。 1507770365237823
transaction_id AdMob 为每个奖励授予事件生成的唯一的十六进制编码标识符。 18fa792de1bca816048293fc71035638
user_id 用户标识符,其提供方法为setUserId

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

1234567

广告联盟标识符

下表列出了 SSV 回调中广告联盟标识符值所对应的广告联盟名称。

广告联盟名称 广告联盟 ID
AdColony 15586990674969969776
AdMob 5450213213286189855
Applovin 1063618907739174004
Chartboost 2873236629771172317
Facebook Audience Network 10568273599589928883
Fuse 8914788932458531264
Fyber 4839637394546996422
InMobi 7681903010231960328
maio 7505118203095108657
myTarget 8450873672465271579
Nend 9383070032774777750
Tapjoy 7295217276740746030
Unity Ads 4970775877303683148
Vungle 1953547073528090325

奖励用户

在决定何时奖励用户时,请务必把握好用户体验和奖励验证之间的平衡。服务器端回调在到达外部系统之前,可能会出现延迟。因此,我们建议的最佳做法是通过客户端回调立即奖励用户,同时在收到服务器端回调时对所有奖励进行验证。这种做法可确保奖励符合授予条件,同时提供良好的用户体验。

但对于某些应用而言,一方面奖励是否符合授予条件至关重要,例如,奖励会影响应用的游戏内经济效益,另一方面又可接受奖励授予上的延迟。这时,最佳做法可能是等待服务器端回调完成验证。

自定义数据

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

以下代码段演示了如何在请求广告之前对激励广告对象设置自定义数据。

RewardedAd rewardedAd = new RewardedAd(this, "ca-app-pub-3940256099942544/5224354917");
ServerSideVerificationOptions options = new ServerSideVerificationOptions.Builder()
    .setCustomData("SAMPLE_CUSTOM_DATA_STRING")
    .build();
rewardedAd.setServerSideVerificationOptions(options);

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

注意:自定义奖励字符串将进行百分比转义,通过 SSV 回调解析时可能需要解码。

手动验证激励视频广告 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, "SHA256WithECDSA");
    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。