Можно найти статьи, датированные 2017...2018 годом, ориентированные на использование сравнительно низкоуровневых средств для отправки и получения веб-пуш сообщений, например, при помощи библиотеки web-push-libs/web-push. Эта библиотека по-прежнему развивается, однако в настоящее время гораздо проще работать с библиотеками от firebase.
Настройка проекта firebase
Итак начнем с создания проекта на firebase. Открыв консоль firebase, необходимо создать новый проект. В разделе Общая информация->Настройки->Общие настройки->Ваши приложения необходимо создать новое веб-приложение. При этом будет сгенерирован код инициализации веб-приложения на стороне фронтенда:
<!-- The core Firebase JS SDK is always required and must be listed first -->
<script src="https://www.gstatic.com/firebasejs/7.19.0/firebase-app.js"></script>
<!-- TODO: Add SDKs for Firebase products that you want to use
https://firebase.google.com/docs/web/setup#available-libraries -->
<script src="https://www.gstatic.com/firebasejs/7.19.0/firebase-analytics.js"></script>
<script>
// Your web app's Firebase configuration
var firebaseConfig = {
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "...",
measurementId: "..."
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
firebase.analytics();
</script>
На этой же вкладке консоли firebase Общая информация->Настройки->Cloud Messaging->Учетные данные для проекта ->Ключ сервера находим приватный ключ, при помощи которого можно отправлять пуши через сервер firebase.
Отправка веб-пуш сообщения
Разработчики фронтенда могут самостоятельно отправлять веб-пуш сообщения при помощи команды curl:
curl -X POST -H "Authorization: key=<Ключ сервера>" -H "Content-Type: application/json" -d '{
"data": {
"title": "FCM Message",
"body": "This is an <i>FCM Message</i>",
"icon": "/static/plus.png",
"sound": "/static/push.mp3",
"click_action": "https://google.com",
},
"to": "<регистрационный токен>"
}' https://fcm.googleapis.com/fcm/send
Получение ключа сервера описано в разделе Настройка проекта firebase, а получение регистрационного токена будет описано в разделе Получение регистрационного токена.
data vs notification payload
Полезные данные могут отправляться в поле data или notification веб-пуш сообщения. Для notification payload запрос будет выглядеть так (для data payload см. запрос в разделе Отправка пуш сообщения):
curl -X POST -H "Authorization: key=<Ключ сервера>" -H "Content-Type: application/json" -d '{
"notification": {
"title": "FCM Message",
"body": "This is an <i>FCM Message</i>",
"icon": "/static/plus.png",
"click_action": "https://google.com",
},
"to": "<регистрационный токен>"
}' https://fcm.googleapis.com/fcm/send
data и notification payload имеют два кардинальных отличия:
- notification payload имеет строго заданный набор полей, лишние поля будут проигнорированы, в то время как data payload передает на фронтенд все поля без ограничения.
- Если веб-браузер находится в бэкграунде или активная ссылка содержит сторонний сайт, веб-пуш с notification payload выводит сообщение, не передавая управление обработчикам события onMessage, в то же время веб-пуш сообщение с data payload всегда передает управление обработчикам события onMessage, но для вывода сообщения необходимо явно создавать объект Notification. Если веб-браузер находится в активном состоянии и на активной вкладке открыт наш сайт, то работа с data и notification payload не различается.
Создание объекта messaging
Для работы на фронтенде с веб-пуш сообщениями необходимо создать объект messaging:
const messaging = window.firebase.messaging();
В данном коде
firebase
это глобальный объект, который создается в процессе загрузок библиотек firebase, и инициализируется на стороне фронтенда как это описано в разделе Настройка проекта firebase. Проект разрабтаывался на Vue.js. Поэтому подключение скриптов через html-элемент script не выглядело перспективным. Для подключения этих скриптов я использовал библиотеку vue-plugin-load-script
:import Vue from "vue";
import LoadScript from "vue-plugin-load-script";
Vue.use(LoadScript);
var firebaseConfig = {
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "...",
measurementId: "..."
};
Promise.resolve()
.then(() =>
Vue.loadScript(
"https://www.gstatic.com/firebasejs/7.14.0/firebase-app.js"
)
)
.then(() =>
Vue.loadScript(
"https://www.gstatic.com/firebasejs/7.14.0/firebase-messaging.js"
)
)
.then(() =>
Vue.loadScript(
"https://www.gstatic.com/firebasejs/7.14.0/firebase-analytics.js"
)
)
.then(() => {
window.firebase.initializeApp(firebaseConfig);
const messaging = window.firebase.messaging();
... // работа с объектом messaging
});
Получение регистрационного токена
Регистрационный токен — это идентификатор который однозначно идентифицирует устройство и веб-браузер, таким образом, позволяя отправлять веб-пуш сообщение на конкретное устройство и обрабатывать его конкретным веб-браузером:
Notification.requestPermission()
.then(permission => {
if (permission === "granted") {
messaging
.getToken()
.then(token => {
... // отправка токена на сервер
});
} else {
console.log("Unable to get permission to notify.");
}
});
При некоторых обстоятельствах токен может обновляться. И необходимо обработать событие обновления токена:
messaging.onTokenRefresh(function() {
messaging
.getToken()
.then(function(refreshedToken) {
... // отправить новый токен на сервер
});
});
По поводу этого события у меня есть вопрос — является ли оно актуальным. Дело в том, что еще до переезда на FCM с GCM работала процедура ротации токенов. Это было описано в библиотеке для Android и косвенно в описании работы сервера, где каждый ответ сервера содержал канонические токены и их нужно было постоянно проверять и менять (впрочем как оказалось кроме меня это редко кто отслеживал). После переезда на FCM такое понятие как канонические токены вышло из употребления (скорее всего потому что на практике их редко кто отслеживал). В связи с этим не совсем ясны случаи когда может возникнуть событие
onTokenRefresh()
.Событие onMessage — упрощенный вариант
Сразу отвечу почему упрощенный. Мы сделаем как минимум два упрощения. 1) Будем использовать notification payload чтобы получать и отображать сообщения, если приложение бэкграунде без дополнительной работы. 2) Забудем, что на мобильных устройствах система защиты не позволяет выполнить оператор new Notification().
Итак, как мы уже сказали, для notification payload веб-пуш сообщение приходит и отображается без малейшего участия фронтенд-разработчика (естественно после отправки на сервер регистрационного токена). Осталось отработать случай веб-браузер находится в активном состоянии и сайт открыт на активной вкладке:
messaging.onMessage(function(payload) {
const data = { ...payload.notification, ...payload.data };
const notificationTitle = data.title;
const notificationOptions = {
body: data.body,
icon: data.icon,
image: data.image,
click_action: data.click_action,
requireInteraction: true,
data
};
new Notification(payload.notification.title, payload.notification);
});
Обработки события получения веб-пуш сообщения в бэкграунде
В этом разделе мы начнем работать с service worker. А это, кроме всего прочего, означает что нужно настроить работу сайта по защищенному протоколу https. И это сразу усложняет дальнейшую разработку. Поэтому для простых случаев достаточно того, что уже было описано раннее.
Для работы с библиотекой firebase по умолчанию используется файл с именем
firebase-messaging-sw.js
. Имя файла можно задать и другое, но он должен в любом случае находиться в корневом каталоге из-за особенностей работы защиты веб-браузера (в противном случае этот serivce worker не будет работать для всего сайта).Как правило в этот файл помещают и обработчик события
notificationclick
. Врядли можно найти что-то отличное от этого кода:var firebaseConfig = {
apiKey: "...",
authDomain: "...",
databaseURL: "...",
projectId: "...",
storageBucket: "...",
messagingSenderId: "...",
appId: "...",
measurementId: "..."
};
importScripts("https://www.gstatic.com/firebasejs/7.17.2/firebase-app.js");
importScripts("https://www.gstatic.com/firebasejs/7.17.2/firebase-messaging.js");
firebase.initializeApp(firebaseConfig);
const messaging = firebase.messaging();
messaging.setBackgroundMessageHandler(function(payload) {
const data = { ...payload.notification, ...payload.data };
const notificationTitle = data.title;
const notificationOptions = {
body: data.body,
icon: data.icon,
image: data.image,
requireInteraction: true,
click_action: data.click_action,
data
};
self.registration.showNotification(notificationTitle, notificationOptions);
});
self.addEventListener("notificationclick", function(event) {
const target = event.notification.data.click_action;
event.notification.close();
event.waitUntil(
clients
.matchAll({
type: "window",
includeUncontrolled: true
})
.then(function(clientList) {
for (var i = 0; i < clientList.length; i++) {
var client = clientList[i];
console.log(client.url, client.focus);
if (client.url === target && "focus" in client) {
return client.focus();
}
}
return clients.openWindow(target);
})
);
});
Вариант обработки события onMessage с service worker
Напоминаю, что в разделе Событие onMessage — упрощенный вариант мы уже описали обработку веб-пуш сообщений. Но у этого способа был один существенный недостаток — он не работал на мобильных устройствах из-за особенностей работы системы защиты веб-браузера. Для преодоления этого недостатка был придуман вариант с service worker, в который уже встроен объект Notification, и его не надо создавать оператором new:
messaging.onMessage(function(payload) {
play();
navigator.serviceWorker.register("/firebase-messaging-sw.js");
Notification.requestPermission(function(result) {
if (result === "granted") {
navigator.serviceWorker.ready
.then(function(registration) {
const data = { ...payload.notification, ...payload.data };
const notificationTitle = data.title;
const notificationOptions = {
body: data.body,
icon: data.icon,
image: data.image,
click_action: data.click_action,
requireInteraction: true,
data
};
return registration.showNotification(
notificationTitle,
notificationOptions
);
})
.catch(function(error) {
console.log("ServiceWorker registration failed", error);
});
}
});
});
Звуковой сигнал при получении веб-пуш сообщения
Надо сказать, что мы практически не можем управлять тем, как пуши будут показываться на различных устройствах. В одних случаях это будет вспылающее сообщение, в других пуш сразу попадает в системную «плашку», и если при этом еще нет никакой озвучки — он будет просто потерян для клиента. Со свуком все очень сложно. В более ранних спецификациях присутствовало поле sound, которое раннее отвечало за звук при пролучении веб-пуш сообщения, но в настоящее время такого свойства нет. В связи с этим я задался целью сделать оцвучку пуша.
Встречающееся иногда описание с созданием html-элеимента audio и вызова его метода play() в реальности не работет из-за особенностей защиты веб-браузера (оно может вызываться только по клику от реального пользователя). Но есть еще AudioContext() — с ним и будем работать:
const play = () => {
try {
const context = new AudioContext();
window
.fetch(soundUrl)
.then(response => response.arrayBuffer())
.then(arrayBuffer => context.decodeAudioData(arrayBuffer))
.then(audioBuffer => {
const source = context.createBufferSource();
source.buffer = audioBuffer;
source.connect(context.destination);
source.start();
});
} catch (ex) {
console.log(ex);
}
};
Все отлично, но у нас еще есть service worker у которых нет объекта AudioContext(). Тут вспомним что все workers общаются через сообщения. И тогда прием событий с service worker будет выглядеть так:
try {
const broadcast = new BroadcastChannel("play");
broadcast.onmessage = play;
} catch (ex) {
console.log(ex) ;
}
Конечно, для работы такого кода нужно чтобы 1) Браузер был открыт 2) Сайт был открыт (хотя не обязательно на активной вкладке). Но иначе никак.
Вместо послесловия
Теперь можно как бы выдохнуть и сказать, все. Но… Все это не работет на safari — и это еще одна отдельная и слабо документировання тема, хотя несколько статей найти можно.
Полезные ссылки
1) habr.com/ru/post/321924
apapacy@gmail.com
24 августа 2020 года
devopg
Если не серверный рендеринг, как защитить " apiKey: "...","?
apapacy Автор
Похоже что никак. В предыдущих версиях кстати достаточно было на фронт отправлять только messageSenderId
Странно что это параметр не считатется секретным. Конечно не зная токена я не смогу отправить сообщение на конкретного клиента. Но зато я смогу направить на любой топик. Не удивлюсь что у 99% сайтов есть топик news
apapacy Автор
Спасибо за вопрос. В тесте статьи была неточность. Ключ api веб-приложения — это публичный ключ и его можно публиковать. Отправлять же сообщения нужно при помощи серверного ключа который имеет другой вид и на другой вкладке находится
devopg
так какая разница?
при запросе к файрбейс, я просто смотрю запрос и вижу всё что есть токены\ключи и т.д.
Беру их и делаю спам атаку.
Причем если передается какойн ибудь userid то делаю перебором какие нибудь фейкс рассылки
apapacy Автор
Не совсем понял механизм такой атаки. Я имею публичные ключи firebase. При помощи этих ключей я могу сделать ровно одно действие — получить токен. Чтобы отправит сообщение нужно знать приватный ключ сервера который в конфиг публичный не входит.