אימות קריאות חוזרות (callbacks) של אימות בצד השרת (SSV)

קריאות חוזרות (callbacks) במסגרת אימות בצד השרת הן בקשות לכתובות URL, עם פרמטרים של שאילתה שמורחבים על ידי Google, שנשלחות על ידי Google למערכת חיצונית כדי להודיע לה שצריך לתגמל משתמש על אינטראקציה עם מודעה מתגמלת או עם מודעה מתגמלת מסוג מודעה מעברון. קריאות חוזרות (callbacks) של SSV (אימות בצד השרת) עם תגמול מספקות שכבת הגנה נוספת מפני זיוף של קריאות חוזרות בצד הלקוח כדי לתגמל משתמשים.

במדריך הזה מוסבר איך לאמת קריאות חוזרות (callbacks) של SSV עם תגמול באמצעות ספריית הצפנה של צד שלישי, Tink Java Apps, כדי לוודא שפרמטרים של שאילתות בקריאה החוזרת הם ערכים חוקיים. למרות שנעשה שימוש ב-Tink למטרות המדריך הזה, יש לכם אפשרות להשתמש בכל ספרייה של צד שלישי שתומכת ב-ECDSA. אפשר גם לבדוק את השרת באמצעות כלי הבדיקה בממשק המשתמש של AdMob.

כדאי לעיין בדוגמה ל-SSV עם פרסים באמצעות Java spring-boot.

דרישות מוקדמות

שימוש ב-RewardedAdsVerifier מספריית האפליקציות של Tink ל-Java

המאגר ב-GitHub של Tink Java Apps כולל את הכיתה המסייעת RewardedAdsVerifier כדי לצמצם את הקוד הנדרש לאימות קריאה חוזרת מסוג SSV עם פרס. השימוש בכיתה הזו מאפשר לאמת כתובת URL להודעת חזרה באמצעות הקוד הבא.

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

אם השיטה verify() פועלת בלי להעלות חריגה, המשמעות היא שהכתובת של ה-callback אומתה בהצלחה. בקטע מתן פרסים למשתמשים מפורטות שיטות מומלצות לגבי הזמנים שבהם כדאי לתת למשתמשים פרסים. בקטע אימות ידני של קריאות חוזרות (callbacks) מסוג SSV עם תגמול מפורט פירוט של השלבים שבהם מבצעת הכיתה הזו את האימות של קריאות חוזרות מסוג SSV עם תגמול.

פרמטרים של קריאה חוזרת (callback) של SSV

קריאות חוזרות (callbacks) במסגרת האימות בצד השרת מכילות פרמטרים של שאילתות שמתארים את האינטראקציה של המשתמש עם מודעת הפרסום המניבה תגמול. בהמשך מפורטים השמות, התיאורים והערכים לדוגמה של הפרמטרים. הפרמטרים נשלחים בסדר אלפביתי.

שם פרמטר תיאור ערך לדוגמה
ad_network מזהה מקור המודעות של מקור המודעות שסיפק את המודעה הזו. השמות של מקורות המודעות שתואמים לערכי המזהים מפורטים בקטע מזהי מקורות מודעות. 1953547073528090325
ad_unit מזהה יחידת המודעות ב-AdMob ששימש לבקשת המודעה המתגמלת. 2747237135
custom_data מחרוזת נתונים בהתאמה אישית כפי שסופקה על ידי setCustomData.

אם האפליקציה לא מספקת מחרוזת נתונים מותאמת אישית, הערך של פרמטר השאילתה הזה לא יופיע בקריאה החוזרת של SSV.

SAMPLE_CUSTOM_DATA_STRING
key_id המפתח שישמש לאימות קריאה חוזרת של SSV. הערך הזה ממופה למפתח ציבורי שמסופק על ידי שרת המפתחות של AdMob. 1234567890
reward_amount סכום התגמול כפי שצוין בהגדרות של יחידת המודעות. 5
reward_item פריט התגמול כפי שצוין בהגדרות של יחידת המודעות. מטבעות
signature חתימה לקריאה חוזרת של SSV שנוצרה על ידי AdMob. MEUCIQCLJS_s4ia_sN06HqzeW7Wc3nhZi4RlW3qV0oO-6AIYdQIgGJEh-rzKreO-paNDbSCzWGMtmgJHYYW9k2_icM9LFMY
חותמת זמן חותמת הזמן של המועד שבו המשתמש קיבל את התגמול, לפי תקופת זמן המערכת (Epoch) באלפיות שנייה. 1507770365237823
transaction_id מזהה ייחודי בקידוד הקסדצימלי לכל אירוע הענקת פרס שנוצר על ידי AdMob. 18fa792de1bca816048293fc71035638
user_id מזהה המשתמש כפי שסופק על ידי setUserId.

אם האפליקציה לא מספקת מזהה משתמש, פרמטר השאילתה הזה לא יופיע בקריאה החוזרת של SSV.

1234567

מזהים של מקורות למודעות

שמות ומזהים של מקורות למודעות

שם מקור המודעה מזהה מקור המודעות
Aarki (בידינג)5240798063227064260
יצירת מודעות (בידינג)1477265452970951479
AdColony15586990674969969776
AdColony (ללא SDK) (בידינג)4600416542059544716
AdColony (בידינג)6895345910719072481
AdFalcon3528208921554210682
רשת AdMob5450213213286189855
Waterfall של רשת AdMob1215381445328257950
ADResult10593873382626181482
AMoAd17253994435944008978
AppLovin1063618907739174004
Applovin (בידינג)1328079684332308356
Chartboost2873236629771172317
Chocolate Platform (בידינג)6432849193975106527
CrossChannel (MdotM)9372067028804390441
אירוע מותאם אישית18351550913290782395
DT Exchange*
* עד 21 בספטמבר 2022, הרשת הזו נקראה 'Fyber Marketplace'.
2179455223494392917
EMX (בידינג)8497809869790333482
Fluct (בידינג)8419777862490735710
Flurry3376427960656545613
Fyber*
* מקור המודעות הזה משמש לדיווח היסטורי.
4839637394546996422
i-mobile5208827440166355534
שיפור הביצועים בדיגיטל (בידינג)159382223051638006
Index Exchange (בידינג)4100650709078789802
InMobi7681903010231960328
InMobi (בידינג)6325663098072678541
InMobi Exchange (בידינג)5264320421916134407
IronSource6925240245545091930
ironSource Ads (בידינג)1643326773739866623
Leadbolt2899150749497968595
LG U+AD18298738678491729107
LINE Ads Network3025503711505004547
maio7505118203095108657
maio (בידינג)1343336733822567166
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
MobFox8079529624516381459
MobFox (בידינג)3086513548163922365
MoPub (הוצאה משימוש)10872986198578383917
myTarget8450873672465271579
Nend9383070032774777750
Nexxen (בידינג)*

* לפני 1 במאי 2024, הרשת הזו נקראה UnrulyX.

2831998725945605450
ONE by AOL‏ (Millennial Media)6101072188699264581
ONE by AOL‏ (Nexage)3224789793037044399
OneTag Exchange (בידינג)4873891452523427499
OpenX (בידינג)4918705482605678398
Pangle4069896914521993236
Pangle (בידינג)3525379893916449117
PubMatic (בידינג)3841544486172445473
קמפיין בהזמנה7068401028668408324
RhythmOne (בידינג)2831998725945605450
Rubicon (בידינג)3993193775968767067
SK planet734341340207269415
Sharethrough (בידינג)5247944089976324188
Smaato (בידינג)3362360112145450544
Equativ (בידינג)*

* עד 12 בינואר 2023, הרשת הזו נקראה 'Smart Adserver'.

5970199210771591442
Sonobi (בידינג)3270984106996027150
Tapjoy7295217276740746030
Tapjoy (בידינג)4692500501762622178
Tencent GDT7007906637038700218
TripleLift (בידינג)8332676245392738510
Unity Ads4970775877303683148
Unity Ads (בידינג)7069338991535737586
Verizon Media7360851262951344112
Verve Group (בידינג)5013176581647059185
Vpon1940957084538325905
Liftoff Monetize*

* עד 30 בינואר 2023, הרשת הזו נקראה 'Vungle'.

1953547073528090325
Liftoff Monetize (בידינג)*

* לפני 30 בינואר 2023, הרשת הזו נקראה 'Vungle (בידינג)'.

4692500501762622185
Yieldmo (בידינג)4193081836471107579
YieldOne (בידינג)3154533971590234104
Zucks5506531810221735863

מתגמלים את המשתמש

חשוב לאזן בין חוויית המשתמש לבין אימות התגמול כשמחליטים מתי לתגמל משתמש. יכול להיות שיחולו עיכובים לפני שהקריאות החוזרות (callbacks) בצד השרת יגיעו למערכות חיצוניות. לכן, השיטה המומלצת היא להשתמש בקריאה החוזרת (callback) בצד הלקוח כדי לתגמל את המשתמש באופן מיידי, ולבצע אימות של כל התגמולים עם קבלת הקריאות החוזרות בצד השרת. הגישה הזו מספקת חוויית משתמש טובה תוך שמירה על תקינות התגמולים.

עם זאת, באפליקציות שבהן תקינות הפרס קריטית (לדוגמה, הפרס משפיע על הכלכלה במשחק של האפליקציה) ועיכובים במתן הפרסים מקובלים, ייתכן שהגישה הטובה ביותר היא להמתין להודעת החזרה (callback) המאומתת בצד השרת.

נתונים בהתאמה אישית

באפליקציות שדורשות נתונים נוספים בקריאות חוזרות (callbacks) לאימות בצד השרת, צריך להשתמש בתכונה 'נתונים מותאמים אישית' של מודעות מתגמלות. כל ערך מחרוזת שמוגדר באובייקט של מודעה עם תגמול מועבר לפרמטר השאילתה custom_data של קריאה חוזרת (callback) של SSV. אם לא מגדירים ערך נתונים מותאם אישית, ערך פרמטר השאילתה custom_data לא יופיע בקריאה החוזרת של SSV.

בדוגמה הבאה מגדירים את האפשרויות של 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
  }
})

אם רוצים להגדיר מחרוזת תגמול בהתאמה אישית, צריך לעשות זאת לפני הצגת המודעה.

אימות ידני של מודעות מתגמלות עם מודעות וידאו

בהמשך מפורטים השלבים שבהם מבצעת הכיתה RewardedAdsVerifier את האימות של SSV עם פרס. קטעי הקוד שכלולים כאן הם ב-Java ומשתמשים בספריית הצד השלישי Tink, אבל אפשר להטמיע את השלבים האלה בשפה שבוחרים, באמצעות כל ספריית צד שלישי שתומכת ב-ECDSA.

אחזור מפתחות ציבוריים

כדי לאמת קריאה חוזרת (callback) מסוג SSV עם תגמול, צריך מפתח ציבורי שמסופק על ידי AdMob.

אפשר לאחזר רשימה של מפתחות ציבוריים שישמשו לאימות הקריאות החוזרות (callbacks) של 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;
}

איך מקבלים תוכן לאימות

שני הפרמטרים האחרונים של השאילתה של קריאות חזרה מסוג 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

הקוד הבא מדגים איך לנתח את התוכן שצריך לאמת מכתובת URL של קריאה חוזרת (callback) כמערך בייטים של 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"));

אחזור החתימה וה-key_id מכתובת ה-URL של הקריאה החוזרת (callback)

בעזרת הערך 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 של קריאת החזרה (callback) כדי לקבל את המפתח הציבורי מהמיפוי הזה. לאחר מכן מאמתים את החתימה באמצעות המפתח הציבורי הזה. השלבים האלה מוצגים בהמשך בשיטה 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 כדי לצמצם את מספר הפעולות הנדרשות לאימות קריאות חזרה מסוג SSV. עם זאת, חשוב לזכור שמפתחות ציבוריים עוברים רוטציה באופן קבוע, ואין לשמור אותם במטמון למשך יותר מ-24 שעות.
באיזו תדירות מתבצעת רוטציה של המפתחות הציבוריים שסופקו על ידי שרת המפתחות של AdMob?
הרוטציה של המפתחות הציבוריים שסופקו על ידי שרת המפתחות של AdMob מתבצעת לפי לוח זמנים משתנה. כדי לוודא שהאימות של קריאות החזרה (callbacks) של SSV ימשיך לפעול כמצופה, אסור לשמור במטמון מפתחות ציבוריים למשך יותר מ-24 שעות.
מה קורה אם לא ניתן לגשת לשרת?
Google מצפה לקוד תגובה עם סטטוס הצלחה HTTP 200 OK בקריאות חזרה של SSV. אם אי אפשר ליצור קשר עם השרת או שהוא לא מספק את התשובה הצפויה, Google תנסה שוב לשלוח קריאות חזרה מסוג SSV עד חמש פעמים במרווחי זמן של שנייה אחת.
איך אפשר לוודא שהשיחות החוזרות של SSV מגיעות מ-Google?
משתמשים בשאילתת DNS הפוכה כדי לוודא שההחזרות של קריאות SSV מגיעות מ-Google.