ניצחונות ביכולת הפעולה ההדדית של Web Push

מאט גאונט
ג'ו מדלי
ג'ו מדלי

כש-Chrome תמך לראשונה ב-Web Push API, השירות היה מבוסס על שירות ה-Push של העברת הודעות בענן ב-Firebase (FCM), שנקרא בעבר העברת הודעות בענן Google (GCM). צריך לעשות זאת באמצעות ממשק ה-API הקנייני שלו. כך Chrome הפך את ה-Web Push API לזמין למפתחים בתקופה שבה המפרט של Web Push Protocol עדיין היה בכתיבה, ובהמשך יסופק אימות (כלומר שולח ההודעה הוא מי שהוא טוען שהוא) בזמן שבו הוא היה חסר. חדשות טובות: אף אחד מהשניים לא נכון יותר.

FCM / GCM ו-Chrome תומכים עכשיו ב-Web Push Protocol הסטנדרטי, בעוד שאימות השולח זמין באמצעות הטמעת VAPID, כלומר באפליקציית האינטרנט שלכם כבר אין צורך ב-'gcm_sender_id'.

במאמר הזה אתאר קודם איך להמיר את קוד השרת הקיים לשימוש בפרוטוקול Web Push עם FCM. לאחר מכן אראה לכם כיצד להטמיע את VAPID גם בקוד הלקוח וגם בקוד השרת.

ב-FCM יש תמיכה בפרוטוקול Web Push Protocol

נתחיל עם מעט הקשר. כשאפליקציית האינטרנט שלכם נרשמת למינוי Push, היא מקבלת את כתובת ה-URL של שירות Push. השרת שלכם ישתמש בנקודת הקצה הזאת כדי לשלוח נתונים למשתמשים דרך אפליקציית האינטרנט. ב-Chrome תקבלו נקודת קצה (endpoint) של FCM אם תירשמו למינוי של משתמש ללא VAPID. (נתייחס ל-VAPID בהמשך). לפני ש-FCM תמך בפרוטוקול Web Push, הייתם צריכים לחלץ את מזהה הרישום של ה-FCM מהסוף של כתובת ה-URL ולהציב אותו בכותרת לפני שליחת בקשת FCM API. לדוגמה, מזהה הרישום של נקודת הקצה של FCM ב-https://android.googleapis.com/gcm/send/ABCD1234 הוא 'ABCD1234'.

עכשיו, כש-FCM תומך בפרוטוקול Web Push, אפשר להשאיר את נקודת הקצה ללא שינוי ולהשתמש בכתובת ה-URL כנקודת קצה של פרוטוקול Web Push. (זאת במקביל ל-Firefox ובתקווה גם לכל הדפדפנים הבאים).

לפני שנצלול ל-VAPID, אנחנו צריכים לוודא שקוד השרת שלנו מטפל בצורה נכונה בנקודת הקצה של FCM. לפניכם דוגמה לשליחת בקשה לשירות דחיפה ב-Node. שימו לב שב-FCM אנחנו מוסיפים את מפתח ה-API לכותרות של הבקשות. אין צורך באפשרות הזו לנקודות קצה אחרות של שירותי Push. ב-Chrome בגרסאות שקודמות לגרסה 52, Opera Android ודפדפן Samsung, עדיין צריך לכלול 'gcm_sender_id' ב-manifest.json של אפליקציית האינטרנט. מפתח ה-API ומזהה השולח משמשים כדי לבדוק אם השרת ששולח את הבקשות אכן מורשה לשלוח הודעות למשתמש המקבל.

const headers = new Headers();
// 12-hour notification time to live.
headers.append('TTL', 12 * 60 * 60);
// Assuming no data is going to be sent
headers.append('Content-Length', 0);

// Assuming you're not using VAPID (read on), this
// proprietary header is needed
if(subscription.endpoint
    .indexOf('https://android.googleapis.com/gcm/send/') === 0) {
    headers.append('Authorization', 'GCM_API_KEY');
}

fetch(subscription.endpoint, {
    method: 'POST',
    headers: headers
})
.then(response => {
    if (response.status !== 201) {
    throw new Error('Unable to send push message');
    }
});

חשוב לזכור שמדובר בשינוי ל-API של FCM או GCM, כך שלא צריך לעדכן את המינויים – אלא רק לשנות את קוד השרת ולהגדיר את הכותרות כפי שמתואר למעלה.

חדש: VAPID לזיהוי שרתים

VAPID הוא השם המקוצר החדש והמגניב של "זיהוי של שרת אפליקציות וולונטרי". המפרט החדש מגדיר לחיצת יד בין שרת האפליקציות לשירות הדחיפה, ומאפשר לשירות הדחיפה לבדוק איזה אתר שולח הודעות. באמצעות VAPID ניתן להימנע מהשלבים הספציפיים ל-FCM לשליחת הודעות Push. אין יותר צורך בפרויקט Firebase, כותרת gcm_sender_id או כותרת Authorization.

התהליך די פשוט:

  1. שרת האפליקציות שלכם יוצר זוג מפתחות ציבורי/פרטי. המפתח הציבורי נשלח לאפליקציית האינטרנט שלכם.
  2. כשהמשתמש בוחר לקבל דחיפה, צריך להוסיף את המפתח הציבורי לאובייקט האפשרויות של הקריאה של Subscribe() .
  3. כששרת האפליקציות שולח הודעת Push, צריך לכלול אסימון רשת JSON חתום יחד עם המפתח הציבורי.

נבחן שלבים אלה בפירוט.

יוצרים זוג מפתחות ציבורי/פרטי

אני ממש גרוע בהצפנה, אז הנה הקטע הרלוונטי במפרט לגבי הפורמט של מפתחות ציבוריים/פרטיים של VAPID:

שרתי האפליקציות צריכים ליצור ולתחזק זוג מפתחות חתימה שאפשר להשתמש בו עם חתימה דיגיטלית עם עקומה אליפטית (ECDSA) לאורך עקומת P-256.

בספריית הצמתים באינטרנט מוסבר איך עושים זאת:

function generateVAPIDKeys() {
    var curve = crypto.createECDH('prime256v1');
    curve.generateKeys();

    return {
    publicKey: curve.getPublicKey(),
    privateKey: curve.getPrivateKey(),
    };
}

הרשמה באמצעות המפתח הציבורי

כדי לרשום משתמש Chrome לדחיפה עם המפתח הציבורי VAPID, צריך להעביר את המפתח הציבורי כ-Uint8Array באמצעות הפרמטר applicationServerKey של השיטה Subscribe() .

const publicKey = new Uint8Array([0x4, 0x37, 0x77, 0xfe, …. ]);
serviceWorkerRegistration.pushManager.subscribe(
    {
    userVisibleOnly: true,
    applicationServerKey: publicKey
    }
);

כדי לדעת אם הוא עובד, בודקים את נקודת הקצה באובייקט המינוי שמתקבל. אם המקור הוא fcm.googleapis.com, הוא פועל.

https://fcm.googleapis.com/fcm/send/ABCD1234

מתבצעת שליחה של הודעה

כדי לשלוח הודעה באמצעות VAPID, עליכם לשלוח בקשת Web Push Protocol רגילה עם שתי כותרות HTTP נוספות: כותרת Authorization וכותרת Crypto-Key.

כותרת הרשאה

הכותרת Authorization היא JSON Web Token (JWT) חתומה עם הכיתוב 'WebPush ' לפניה.

JWT מאפשר לשתף אובייקט JSON עם צד שני, כך שהצד השולח יוכל לחתום עליו והצד המקבל יוכל לאמת שהחתימה נשלחה מהשולח הצפוי. המבנה של JWT מורכב משלוש מחרוזות מוצפנות, שמחוברות ביניהן נקודה אחת.

<JWTHeader>.<Payload>.<Signature>

כותרת JWT

כותרת ה-JWT מכילה את שם האלגוריתם שמשמש לחתימה ואת סוג האסימון. ל-VAPID חייב להיות:

{
    "typ": "JWT",
    "alg": "ES256"
}

לאחר מכן, כתובת ה-URL הזו מקודדת ב-base64 ויוצרת את החלק הראשון של ה-JWT.

מטען ייעודי (payload)

המטען הייעודי הוא אובייקט JSON נוסף שמכיל את הרכיבים הבאים:

  • קהל (aud)
    • זהו המקור של שירות ה-Push (לא מקור האתר שלך). ב-JavaScript, אפשר לבצע את הפעולות הבאות כדי לקבל את הקהל: const audience = new URL(subscription.endpoint).origin
  • מועד תפוגה ("exp")
    • זהו מספר השניות עד שהבקשה תקפה. חובה לעשות זאת תוך 24 שעות ממועד שליחת הבקשה, לפי שעון UTC.
  • נושא ("sub")
    • הנושא צריך להיות כתובת URL או כתובת URL מסוג mailto:. כך אפשר לפנות לאיש קשר למקרה ששירות ה-Push צריך ליצור קשר עם שולח ההודעה.

מטען ייעודי (payload) לדוגמה יכול להיראות כך:

{
    "aud": "http://push-service.example.com",
    "exp": Math.floor((Date.now() / 1000) + (12 * 60 * 60)),
    "sub": "mailto: my-email@some-url.com"
}

אובייקט JSON זה מקודד כתובת URL מסוג base64 ויוצר את החלק השני של ה-JWT.

חתימה

החתימה נוצרת כתוצאה מאיחוד הכותרת המקודדת והמטען הייעודי (payload) עם נקודה, ולאחר מכן הצפנת התוצאה באמצעות המפתח הפרטי VAPID שיצרתם קודם לכן. יש לצרף את התוצאה עצמה לכותרת באמצעות נקודה.

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

ה-JWT החתום משמש ככותרת Authorization עם הצירוף 'WebPush ' בתחילתו, והוא אמור להיראות בערך כך:

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

יש כמה דברים שחשוב לדעת בנושא. ראשית, הכותרת Authorization מכילה באופן מילולי את המילה WebPush, ואחריה יופיע רווח ולאחר מכן ה-JWT. כמו כן, שימו לב לנקודות שמפרידות בין הכותרת, המטען הייעודי (payload) והחתימה של JWT.

כותרת Crypto-Key

בנוסף לכותרת Authorization, עליכם להוסיף את המפתח הציבורי VAPID לכותרת Crypto-Key כמחרוזת עם כתובת URL מסוג base64 עם הקידומת p256ecdsa= בתחילתה.

p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo

כששולחים התראה עם נתונים מוצפנים, כבר משתמשים בכותרת Crypto-Key, אז כדי להוסיף את מפתח שרת האפליקציות צריך רק להוסיף נקודה ופסיק לפני הוספת התוכן שלמעלה, והתוצאה תהיה:

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S_k7zi7A1coR_sVjHmGrlvzYpAT1n4NPbioFlQkIrTNL8EH4V3ZZ4vJE;
p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaN

מציאות השינויים האלה

עם VAPID לא צריך יותר להירשם לחשבון ב-GCM כדי להשתמש ב-Push ב-Chrome, ותוכלו להשתמש באותו נתיב קוד לרישום משתמש ולשליחת הודעה למשתמש גם ב-Chrome וגם ב-Firefox. שניהם עומדים בסטנדרטים.

חשוב לזכור שב-Chrome 51 ובגרסאות קודמות, דפדפן Opera לדפדפן Android ו-Samsung עדיין צריך להגדיר את gcm_sender_id במניפסט של אפליקציית האינטרנט, וצריך להוסיף את הכותרת Authorization לנקודת הקצה של FCM שתוחזר.

VAPID מספק נקודת יציאה מהדרישות הקנייניות האלה. אם תטמיעו VAPID, הוא יפעל בכל הדפדפנים שתומכים בדחיפה באינטרנט. ככל שדפדפנים נוספים תומכים ב-VAPID, תוכלו להחליט מתי להסיר את gcm_sender_id מהמניפסט.