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


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

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

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

שימוש ב-RewardedAdsVerifier מספריית Tink Java Apps

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

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

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

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

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

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

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

1234567

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

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

שם מקור המודעה מזהה מקור המודעות
Ad Generation (bidding)1477265452970951479
AdColony15586990674969969776
AdColony (בידינג)6895345910719072481
AdFalcon3528208921554210682
רשת AdMob5450213213286189855
AdMob Network Waterfall1215381445328257950
Applovin1063618907739174004
Applovin (בידינג)1328079684332308356
Chartboost2873236629771172317
Chocolate Platform (bidding)6432849193975106527
אירוע מותאם אישית18351550913290782395
DT Exchange*
* עד 21 בספטמבר 2022, הרשת הזו נקראה Fyber Marketplace.
2179455223494392917
‫Equativ (בידינג)*

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

5970199210771591442
‫Fluct (בידינג)8419777862490735710
Flurry3376427960656545613
‫Fyber*
* מקור המודעות הזה משמש לדיווח היסטורי.
4839637394546996422
i-mobile5208827440166355534
Improve Digital (בידינג)159382223051638006
‫Index Exchange (בידינג)4100650709078789802
InMobi7681903010231960328
InMobi (בידינג)6325663098072678541
InMobi Exchange (בידינג)5264320421916134407
IronSource6925240245545091930
‫ironSource Ads (בידינג)1643326773739866623
Leadbolt2899150749497968595
Liftoff Monetize*

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

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

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

4692500501762622185
LG U+AD18298738678491729107
LINE Ads Network3025503711505004547
Magnite DV+‎ (בידינג)3993193775968767067
maio7505118203095108657
maio (bidding)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
‫MobFox (בידינג)3086513548163922365
‫MoPub (הוצאה משימוש)10872986198578383917
myTarget8450873672465271579
Nend9383070032774777750
Nexxen (בידינג)*

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

2831998725945605450
OneTag Exchange (בידינג)4873891452523427499
OpenX (בידינג)4918705482605678398
Pangle4069896914521993236
Pangle (בידינג)3525379893916449117
PubMatic (בידינג)3841544486172445473
קמפיין בהזמנה7068401028668408324
SK planet734341340207269415
Sharethrough (בידינג)5247944089976324188
Smaato (בידינג)3362360112145450544
Sonobi (בידינג)3270984106996027150
Tapjoy7295217276740746030
Tapjoy (בידינג)4692500501762622178
Tencent GDT7007906637038700218
TripleLift (bidding)8332676245392738510
Unity Ads4970775877303683148
Unity Ads (בידינג)7069338991535737586
Verve Group (בידינג)5013176581647059185
Vpon1940957084538325905
Yieldmo (בידינג)4193081836471107579
YieldOne (בידינג)3154533971590234104
Zucks5506531810221735863

תגמול המשתמש

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

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

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

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

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

  firebase::gma::RewardedAd* rewarded_ad;
  rewarded_ad = new firebase::gma::RewardedAd();

  firebase::gma::RewardedAd::ServerSideVerificationOptions options;
  options.custom_data = "SAMPLE_CUSTOM_DATA_STRING";
  rewarded_ad->SetServerSideVerificationOptions(options);

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

אימות ידני של אימות בצד השרת לצפייה במודעות מתגמלות

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

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

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

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

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

שני הפרמטרים האחרונים של שאילתות בקריאות חוזרות (callback) של SSV עם תגמול הם תמיד signature ו-key_id,, בסדר הזה. בשאר הפרמטרים של השאילתה מצוין התוכן שצריך לאמת. נניח שהגדרתם את AdMob לשליחת קריאות חוזרות (callback) על תגמולים אל https://www.myserver.com/mypath. קטע הקוד שלמטה מציג דוגמה לקריאה חוזרת (callback) של 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 של קריאה חוזרת כמערך בייטים בקידוד 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 לקריאה חוזרת (callback) באמצעות המפתח הציבורי המתאים. לוקחים את המיפוי שמוחזר מהשיטה 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 לקריאה חוזרת (callback) אומתה בהצלחה.

שאלות נפוצות

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