Как проверять обратные вызовы при проверке на стороне сервера

Обратные вызовы проверки на стороне сервера – это URL-запросы с параметрами запроса, которые Google добавляет в URL. Они отправляются во внешнюю систему, чтобы сообщить ей, что пользователь должен получить вознаграждение за взаимодействие с объявлением с вознаграждением или межстраничным объявлением с вознаграждением. Обратные вызовы для проверки на стороне сервера при показе объявлений с вознаграждением обеспечивают дополнительную защиту от спуфинга клиентских обратных вызовов для предоставления вознаграждений пользователям.

В этом руководстве рассказывается, как проверять обратные вызовы для объявлений с вознаграждением, используя стороннюю криптографическую библиотеку Tink Java Apps. Это позволяет убедиться, что параметры запроса в обратном вызове содержат допустимые значения. В этом руководстве используется библиотека Tink, но вы можете выбрать любую стороннюю библиотеку, которая поддерживает ECDSA. Вы также можете проверить свой сервер с помощью инструмента тестирования в интерфейсе AdMob.

Требования

Используйте RewardedAdsVerifier из библиотеки приложений Tink Java

В хранилище GitHub Tink Java Apps есть вспомогательный класс RewardedAdsVerifier, который позволяет сократить код, необходимый для проверки обратного вызова при показе объявлений с вознаграждением. Используя этот класс, вы можете проверить URL обратного вызова с помощью следующего кода.

RewardedAdsVerifier verifier = new RewardedAdsVerifier.Builder()
    .fetchVerifyingPublicKeysWith(
        RewardedAdsVerifier.KEYS_DOWNLOADER_INSTANCE_PROD)
    .build();
String rewardUrl = ...;
verifier.verify(rewardUrl);

Если метод verify() выполняется без исключений, URL обратного вызова успешно подтвержден. В разделе Вознаграждение пользователей приведены рекомендации о том, когда следует предоставлять вознаграждение. Подробное описание действий, выполняемых этим классом для проверки обратных вызовов SSV, приведено в разделе Проверка обратных вызовов SSV вручную.

Параметры обратного вызова для проверки на стороне сервера

Обратные вызовы проверки на стороне сервера содержат параметры запроса, описывающие взаимодействие с объявлением с вознаграждением. Ниже приведены названия, описания и примеры значений параметров. Параметры отправляются в алфавитном порядке.

Название параметра Описание Пример значения
ad_network Идентификатор источника, из которого было получено объявление. Названия источников объявлений, соответствующие значениям идентификаторов, перечислены в разделе Идентификаторы источников объявлений. 1953547073528090325
ad_unit Идентификатор рекламного блока AdMob, который использовался для запроса объявления с вознаграждением. 2747237135
key_id Ключ, который будет использоваться для проверки обратного вызова SSV. Это значение сопоставляется с открытым ключом, предоставленным сервером ключей AdMob. 1234567890
reward_amount Размер вознаграждения, указанный в настройках рекламного блока. 5
reward_item Предмет вознаграждения, указанный в настройках рекламного блока. монеты
подпись Подпись для обратного вызова SSV, созданная AdMob. MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY
временная метка Временная метка, указывающая, когда пользователь получил вознаграждение (в миллисекундах). 1507770365237823
transaction_id Уникальный шестнадцатеричный идентификатор для каждого события предоставления вознаграждения, созданного AdMob. 18fa792de1bca816048293fc71035638
user_id Идентификатор пользователя, предоставленный SetUserId.

Если приложение не предоставляет идентификатор пользователя, этот параметр запроса не будет присутствовать в обратном вызове для проверки на стороне сервера.

1234567

Идентификаторы источников объявлений

Названия и идентификаторы источников объявлений.

Название источника объявления Идентификатор источника объявлений
Ad Generation (назначение ставок)1477265452970951479
Сеть AdMob5450213213286189855
Каскад сети AdMob1215381445328257950
AppLovin1063618907739174004
AppLovin (назначение ставок)1328079684332308356
Bidease (назначение ставок)3670825090829827805
BidMachine (назначение ставок)7943972370566394673
Chartboost2873236629771172317
Chocolate Platform (назначение ставок)6432849193975106527
Пользовательское событие18351550913290782395
DT Exchange*

* До 21 сентября 2022 г. эта сеть называлась Fyber Marketplace.

2179455223494392917
DT Exchange (назначение ставок)8189833498765234879
Equativ (назначение ставок)*

* До 12 января 2023 г. эта сеть называлась Smart Adserver.

5970199210771591442
Fluct (назначение ставок)8419777862490735710
i-mobile5208827440166355534
Improve Digital (назначение ставок)159382223051638006
Index Exchange (назначение ставок)4100650709078789802
InMobi7681903010231960328
InMobi (SDK) (назначение ставок)8468954295581492586
InMobi Exchange (назначение ставок)5264320421916134407
Реклама в ironSource6925240245545091930
ironSource Ads (назначение ставок)1643326773739866623
Liftoff Monetize*

* До 30 января 2023 г. эта сеть называлась Vungle.

1953547073528090325
Liftoff Monetize (назначение ставок)*

* До 30 января 2023 г. эта сеть называлась "Vungle (назначение ставок)".

4692500501762622185
LY Ads Network3025503711505004547
LY Ads Network (назначение ставок)2615812619460460513
Magnite DV+ (назначение ставок)3993193775968767067
maio7505118203095108657
Media.net (назначение ставок)2127936450554446159
Собственные объявления с медиацией6060308706800320801
Meta Audience Network*

* До 6 июня 2022 г. эта сеть называлась Facebook Audience Network.

10568273599589928883
Meta Audience Network (назначение ставок)*

* До 6 июня 2022 г. эта сеть называлась Facebook Audience Network (назначение ставок).

11198165126854996598
Mintegral1357746574408896200
Mintegral (назначение ставок)6250601289653372374
Mobfox (назначение ставок)3086513548163922365
MobileFuse (назначение ставок)7303547408604090310
Moloco Ads SDK (назначение ставок)8267622065755668722
myTarget8450873672465271579
Nativo (назначение ставок)3240503836211327896
Nexxen (назначение ставок)*

* До 1 мая 2024 г. эта сеть называлась UnrulyX.

2831998725945605450
OneTag Exchange (назначение ставок)4873891452523427499
OpenX (назначение ставок)4918705482605678398
Pangle4069896914521993236
Pangle KR SDK (назначение ставок)12171279046073404914
Pangle ROW SDK (назначение ставок)3525379893916449117
Pangle US SDK (назначение ставок)15999446638585856012
PubMatic (назначение ставок)3841544486172445473
PubMatic OpenWrap SDK7702975372504485373
PubMatic OpenWrap SDK (назначение ставок)1234567890123456789
Кампания с резервированием7068401028668408324
Rise (назначение ставок)6816468518946650043
Sharethrough (назначение ставок)5247944089976324188
Smaato (назначение ставок)3362360112145450544
Sonobi (назначение ставок)3270984106996027150
TripleLift (назначение ставок)8332676245392738510
Unity Ads4970775877303683148
Unity Ads (назначение ставок)7069338991535737586
Verve Group (назначение ставок)5013176581647059185
Vpon1940957084538325905
Yieldmo (назначение ставок)4193081836471107579
YieldOne (назначение ставок)3154533971590234104
Zucks5506531810221735863

Вознаграждение пользователя

Принимая решение о том, когда предоставлять вознаграждение, важно соблюдать баланс между удобством для пользователей и проверкой вознаграждения. Обратные вызовы на сервере могут поступать во внешние системы с задержкой. Поэтому мы рекомендуем использовать клиентский обратный вызов, чтобы сразу же предоставить пользователю вознаграждение, а затем проверять все вознаграждения при получении серверных обратных вызовов. Такой подход позволяет обеспечить удобство для пользователей и гарантировать, что вознаграждения будут предоставляться только за действительные просмотры.

Однако если для приложения критически важна действительность вознаграждения (например, оно влияет на экономику игры) и задержки в предоставлении вознаграждений допустимы, то лучше дождаться подтвержденного обратного вызова со стороны сервера.

Специальные данные

Если приложению требуются дополнительные данные в обратных вызовах для проверки на стороне сервера, используйте функцию специальных данных в объявлениях с вознаграждением. Любое строковое значение, заданное в объекте объявления с вознаграждением, передается в параметр запроса custom_data обратного вызова для проверки на стороне сервера. Если значение данных клиентов не задано, то значение параметра запроса custom_data не будет представлено в обратном вызове для проверки на стороне сервера.

В приведенном ниже примере кода показано, как задать параметры проверки на стороне сервера после загрузки объявления с вознаграждением.

private void LoadRewardedAd(string adUnitId)
{
  // Send the request to load the ad.
  AdRequest adRequest = new AdRequest();
  RewardedAd.Load(adUnitId, adRequest, (RewardedAd rewardedAd, LoadAdError error) =>
  {
    // If the operation failed with a reason.
    if (error != null)
    {
        Debug.LogError("Rewarded ad failed to load an ad with error : " + error);
        return;
    }

    var options = new ServerSideVerificationOptions
                          .Builder()
                          .SetCustomData("SAMPLE_CUSTOM_DATA_STRING")
                          .Build()
    rewardedAd.SetServerSideVerificationOptions(options);
  });
}

Если вы хотите задать собственную строку вознаграждения, это нужно сделать до показа объявления.

Проверка вручную для объявлений с вознаграждением

Ниже описаны действия, которые выполняет класс RewardedAdsVerifier для проверки SSV-запроса. Приведенные ниже фрагменты кода написаны на Java и используют стороннюю библиотеку Tink, но вы можете реализовать эти шаги на любом языке программирования, используя любую стороннюю библиотеку, которая поддерживает ECDSA.

Получение открытых ключей

Чтобы проверить обратный вызов для объявления с вознаграждением, вам потребуется открытый ключ, предоставленный 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 с открытыми ключами, которые инкапсулированы как объекты ECPublicKey из библиотеки Tink.

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;
}

Как получить контент для проверки

Последними двумя параметрами запроса в обратных вызовах для проверки на стороне сервера всегда являются signature и key_id, (именно в таком порядке). Остальные параметры запроса указывают на контент, который нужно проверить. Допустим, вы настроили AdMob для отправки обратных вызовов на URL https://www.myserver.com/mypath. Ниже приведен пример обратного вызова для проверки на стороне сервера с выделенным контентом, который нужно проверить.

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

В примере кода ниже показано, как проанализировать контент, который нужно проверить, из URL обратного вызова в виде массива байтов 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"));

Получите подпись и идентификатор ключа из URL обратного вызова

Используя значение queryString из предыдущего шага, проанализируйте параметры запроса signature и key_id из URL обратного вызова, как показано ниже:

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()));

Выполнить проверку

Последний шаг – проверка контента URL обратного вызова с помощью соответствующего открытого ключа. Возьмите сопоставление, возвращенное методом parsePublicKeysJson, и используйте параметр key_id из URL обратного вызова, чтобы получить открытый ключ из этого сопоставления. Затем проверьте подпись с помощью этого открытого ключа. Эти шаги показаны ниже в методе 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);
  }
}

Если метод выполняется без исключений, URL обратного вызова успешно подтвержден.

Часто задаваемые вопросы

Можно ли кешировать открытый ключ, предоставленный сервером ключей AdMob?
Рекомендуем кешировать открытый ключ, предоставленный сервером ключей AdMob, чтобы сократить количество операций, необходимых для проверки обратных вызовов для проверки на сервере. Однако помните, что открытые ключи регулярно меняются и не должны кешироваться более чем на 24 часа.
Как часто меняются открытые ключи, предоставляемые сервером ключей AdMob?
Открытые ключи, предоставляемые сервером ключей AdMob, меняются по переменному расписанию. Чтобы проверка обратных вызовов для вознаграждения на стороне сервера работала корректно, открытые ключи не должны кешироваться дольше чем на 24 часа.
Что произойдет, если мой сервер будет недоступен?
Google ожидает код ответа HTTP 200 OK для обратных вызовов SSV. Если ваш сервер недоступен или не возвращает ожидаемый ответ, Google попытается отправить обратные вызовы для проверки на стороне сервера до пяти раз с интервалом в одну секунду.
Как проверить, что обратные вызовы для проверки на стороне сервера поступают от Google?
Используйте обратный DNS-запрос, чтобы убедиться, что обратные вызовы для проверки на стороне сервера поступают от Google.