כש-Chrome התחיל לתמוך ב-Web Push API, הוא הסתמך על שירות ה-push של Firebase Cloud Messaging (FCM), שנקרא בעבר Google Cloud Messaging (GCM). לשם כך היה צורך להשתמש ב-API הקנייני שלו. כך הצלחנו להפוך את Web Push API לזמין למפתחים ב-Chrome בזמן שעדיין נכתבה המפרט של פרוטוקול Web Push, ולאחר מכן לספק אימות (כלומר, ששולח ההודעה הוא מי שהוא מציג את עצמו) בזמן שפרוטוקול Web Push לא כלל אימות. חדשות טובות: אף אחת מהאפשרויות האלה לא נכונה.
FCM / GCM ו-Chrome תומכים עכשיו ב-Web Push Protocol הסטנדרטי, ואילו אימות השולח אפשר להתבצע על ידי הטמעת VAPID, כלומר אפליקציית האינטרנט שלך כבר לא זקוקה ל-'gcm_sender_id'.
במאמר הזה, קודם אסביר איך להמיר את קוד השרת הקיים כך שישתמש בפרוטוקול Web Push עם FCM. בשלב הבא אראה לכם איך להטמיע VAPID גם בקוד הלקוח וגם בקוד השרת.
FCM תומך בפרוטוקול Web Push
נתחיל עם קצת הקשר. כשאפליקציית האינטרנט נרשמת למינוי לקבלת התראות, היא מקבלת את כתובת ה-URL של שירות ההתראות. השרת ישתמש בנקודת הקצה הזו כדי לשלוח נתונים למשתמש דרך אפליקציית האינטרנט. ב-Chrome, תקבלו נקודת קצה של FCM אם תירשמו משתמש ללא VAPID. (נדון ב-VPAID בהמשך). לפני ש-FCM תמך בפרוטוקול Web Push, צריך היה לחלץ את מזהה ההרשמה ל-FCM מסוף כתובת ה-URL ולהוסיף אותו לכותרת לפני שליחת בקשה ל-FCM API. לדוגמה, לנקודת קצה של FCM https://android.googleapis.com/gcm/send/ABCD1234
יהיה מזהה רישום ABCD1234.
עכשיו, כש-FCM תומך בפרוטוקול Web Push, אפשר להשאיר את נקודת הקצה ללא שינוי ולהשתמש בכתובת ה-URL כנקודת קצה לפרוטוקול Web Push. (כך הוא תואם ל-Firefox, ואנחנו מקווים שגם לכל דפדפן עתידי אחר).
לפני שנצלול לתוך VAPID, אנחנו צריכים לוודא שקוד השרת שלנו מטפל בנקודת הקצה (endpoint) של 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');
}
});
חשוב לזכור שמדובר בשינוי ל-FCM / GCM's API, כך שלא צריך לעדכן את המינויים. צריך רק לשנות את קוד השרת ולהגדיר את הכותרות כפי שמוצג למעלה.
חדש: VAPID לזיהוי שרתים
VAPID הוא השם המקוצר החדש והמגניב של Voluntary Application Server Identification (זיהוי שרתי אפליקציות מרצון). המפרט החדש הזה מגדיר למעשה לחיצת יד בין שרת האפליקציה שלכם לבין שירות ה-Push, ומאפשר לשירות ה-Push כדי לאמת איזה אתר שולח הודעות.
בעזרת VAPID אפשר להימנע מהשלבים הספציפיים ל-FCM לשליחת הודעת דחיפה. אין יותר צורך בפרויקט ב-Firebase, ב-gcm_sender_id
או בכותרת Authorization
.
התהליך פשוט למדי:
- שרת האפליקציות יוצר זוג מפתחות – ציבורי ופרטי. המפתח הציבורי ניתן לאפליקציית האינטרנט.
- כשהמשתמש בוחר לקבל התראות דחיפה, מוסיפים את המפתח הציבורי לאובייקט האפשרויות של הקריאה subscribe().
- כששרת האפליקציות שולח הודעת דחיפה, צריך לכלול אסימון אינטרנט חתום מסוג JSON יחד עם המפתח הציבורי.
נבחן את השלבים האלה לעומק.
יצירת זוג מפתחות ציבורי/פרטי
אני לא מתמצא בהצפנה, אז הנה הקטע הרלוונטי מהמפרט לגבי הפורמט של המפתחות הציבוריים/הפרטיים של VAPID:
רצוי שרשתות של שרתים לאפליקציות יפיקו וינהלו זוג מפתחות לחתימה שאפשר להשתמש בו עם חתימה דיגיטלית על עקומה אליפטית (ECDSA) על עקומה P-256.
בספריית הצמתים של דחיפה ל-web-push מוסבר איך עושים זאת:
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 נוספות: כותרת הרשאה וכותרת Crypto-Key.
כותרת Authorization
הכותרת Authorization
היא אסימון אינטרנט מסוג JSON (JWT) חתום, ולפניו מופיע הכיתוב WebPush.
JWT הוא דרך לשתף אובייקט JSON עם צד שני כך שצד השולח יכול לחתום עליו וצד המקבל יכול לאמת שהחתימה היא מהשולח הצפוי. המבנה של JWT הוא שלוש מחרוזות מוצפנות שמחוברות באמצעות נקודה אחת.
<JWTHeader>.<Payload>.<Signature>
כותרת JWT
הכותרת של JWT מכילה את שם האלגוריתם ששימש לחתימה ואת סוג האסימון. ב-VAPID, הערך הזה חייב להיות:
{
"typ": "JWT",
"alg": "ES256"
}
לאחר מכן הוא מקודד ב-base64 ככתובת URL ומהווה את החלק הראשון של ה-JWT.
מטען ייעודי (payload)
המטען הייעודי הוא אובייקט JSON נוסף שמכיל את הפרטים הבאים:
- קהל ('aud')
- זהו המקור של שירות ה-push (לא המקור של האתר).
ב-JavaScript, אפשר לבצע את הפעולות הבאות כדי לקבל את הקהל:
const audience = new URL(subscription.endpoint).origin
- זהו המקור של שירות ה-push (לא המקור של האתר).
ב-JavaScript, אפשר לבצע את הפעולות הבאות כדי לקבל את הקהל:
- מועד התפוגה ("exp")
- זהו מספר השניות עד שהבקשה תחשב כפגת תוקף. חובה לשלוח את הבקשה תוך 24 שעות ממועד שליחת הבקשה, לפי שעון UTC.
- נושא ('sub')
- הנושא צריך להיות כתובת URL או כתובת URL מסוג
mailto:
. זהו איש קשר למקרה ששירות ה-push צריך ליצור קשר עם שולח ההודעה.
- הנושא צריך להיות כתובת URL או כתובת URL מסוג
דוגמה למטען ייעודי (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.
חתימה
החתימה היא התוצאה של צירוף הכותרת והמטען הקונטייננטי המוצפנים באמצעות נקודה, ולאחר מכן הצפנת התוצאה באמצעות המפתח הפרטי של VAPID שיצרתם קודם. התוצאה עצמה צריכה להתווסף לכותרת באמצעות נקודה.
לא אציג קוד לדוגמה לכך, כי יש מספר ספריות שיקבלו את אובייקטי ה-JSON של הכותרת והמטען הייעודי וייצרו בשבילכם את החתימה הזו.
ה-JWT החתום משמש ככותרת ההרשאה עם 'WebPush' לפניו, והוא ייראה בערך כך:
WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A
חשוב לשים לב לכמה דברים בנושא הזה. קודם כול, כותרת ההרשאה מכילה את המילה 'WebPush', ואחריה צריך להופיע רווח ואז ה-JWT. שימו לב גם לנקודות שמפרידות בין הכותרת, המטען הייעודי (payload) והחתימה.
כותרת של 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 כדי להשתמש בהתראות דחיפה ב-Chrome, וניתן להשתמש באותו נתיב קוד כדי להירשם משתמש ולשלוח הודעה למשתמש גם ב-Chrome וגם ב-Firefox. שניהם עומדים בסטנדרטים.
חשוב לזכור שבגרסה 51 של Chrome ובגרסאות קודמות, ב-Opera ל-Android ובדפדפן של Samsung, עדיין צריך להגדיר את gcm_sender_id
במניפסט של אפליקציית האינטרנט, ולהוסיף את כותרת ההרשאה לנקודת הקצה של FCM שתוחזר.
VAPID מספק דרך חלופית לדרישות הקנייניות האלה. אם תטמיעו ב-VAPID, הוא יפעל בכל הדפדפנים שתומכים בהתראות דחיפה לאינטרנט. ככל שיותר דפדפנים יתמכו ב-VAPID, תוכלו להחליט מתי להסיר את gcm_sender_id
מהמאניפסט.