- Рассылка Push-уведомлений с SpringBoot сервера
- Непосредственно к сути
- Подводим итоги
- Как отправлять веб-push-уведомления на Java
- Видеоверсия
- Немного предыстории: как работают веб-push-уведомления
- Подписка на push-уведомления
- Отправка push-уведомлений
- Настройте проект и сгенерируйте ДЕЙСТВИТЕЛЬНЫЕ ключи
- Создайте службу Java для обработки подписок и отправки уведомлений
- Создайте конечную точку для доступа к серверу
- Подписаться на уведомления в браузере
- Web Push Notifications 📣
- Обрабатывать входящие push-сообщения в Сервисном работнике
- Исходный код
- Читайте ещё по теме:
Рассылка Push-уведомлений с SpringBoot сервера
Приветствую Вас. Недавно передо мной стала задача — настроить Push-уведомления на сайте. С этим я столкнулся впервые и во много разобраться мне помогла эта статья. В ней же уже есть описание серверной стороны, но, в процессе изучения данной темы я обнаружил более удобный способ реализации средствами самой библиотеки Firebase. Собственно, о нем я и хотел бы вам рассказать, т.к. внятного объяснения в интернете мне не удалось найти.
Также данная статья может быть полезна программирующим на Node.js, Python и Go, поскольку библиотека есть и на этих языках.
Непосредственно к сути
В данной статье я расскажу только о серверной стороне.
(клиентскую часть Вы можете настроить используя ту самую статью)
- Для начала Вам нужно зайти на сайт, зарегистрировать и создать проект.
- Далее в левом верхнем углу нажимаем на шестерню и выбираем «Настройки проекта».
- Переходим на вкладку «Сервисные аккаунты», выбираем интересующий нас язык, нажимаем на «создание закрытого ключа» и скачиваем сгенерированный файл
Теперь займемся сервером
Для удобства объявим в application.properties путь к скаченному файлу
fcm.service-account-file = /path/to/file.json
Добавим необходимые зависимости в pom.xml
com.google.firebase firebase-admin 6.7.0
Создадим бин возвращающий наш JSON:
@ConfigurationProperties(prefix = "fcm") @Component public class FcmSettings < private String serviceAccountFile; public String getServiceAccountFile() < return this.serviceAccountFile; >public void setServiceAccountFile(String serviceAccountFile) < this.serviceAccountFile = serviceAccountFile; >>
@Getter @Setter public class PushNotifyConf < private String title; private String body; private String icon; private String click_action; private String ttlInSeconds; public PushNotifyConf() < >public PushNotifyConf(String title, String body, String icon, String click_action, String ttlInSeconds) < this.title = title; this.body = body; this.icon = icon; this.click_action = click_action; this.ttlInSeconds = ttlInSeconds; >>
- title — Оглавление уведомления
- body — текст уведомления
- icon — ссылка на картинку
- click_action — ссылка, куда отправится пользователь при клике на уведомление (с названием, пример в сервисе)
И сервис, в котором и будет вся логика отправки уведомлений:
@Service public class FcmClient < public FcmClient(FcmSettings settings) < Path p = Paths.get(settings.getServiceAccountFile()); try (InputStream serviceAccount = Files.newInputStream(p)) < FirebaseOptions options = new FirebaseOptions.Builder() .setCredentials(GoogleCredentials.fromStream(serviceAccount)) .build(); FirebaseApp.initializeApp(options); >catch (IOException e) < Logger.getLogger(FcmClient.class.getName()) .log(Level.SEVERE, null, e); >> public String sendByTopic(PushNotifyConf conf, String topic) throws InterruptedException, ExecutionException < Message message = Message.builder().setTopic(topic) .setWebpushConfig(WebpushConfig.builder() .putHeader("ttl", conf.getTtlInSeconds()) .setNotification(createBuilder(conf).build()) .build()) .build(); String response = FirebaseMessaging.getInstance() .sendAsync(message) .get(); return response; >public String sendPersonal(PushNotifyConf conf, String clientToken) throws ExecutionException, InterruptedException < Message message = Message.builder().setToken(clientToken) .setWebpushConfig(WebpushConfig.builder() .putHeader("ttl", conf.getTtlInSeconds()) .setNotification(createBuilder(conf).build()) .build()) .build(); String response = FirebaseMessaging.getInstance() .sendAsync(message) .get(); return response; >public void subscribeUsers(String topic, List clientTokens) throws FirebaseMessagingException < for (String token : clientTokens) < TopicManagementResponse response = FirebaseMessaging.getInstance() .subscribeToTopic(Collections.singletonList(token), topic); >> private WebpushNotification.Builder createBuilder(PushNotifyConf conf) < WebpushNotification.Builder builder = WebpushNotification.builder(); builder.addAction(new WebpushNotification .Action(conf.getClick_action(), "Открыть")) .setImage(conf.getIcon()) .setTitle(conf.getTitle()) .setBody(conf.getBody()); return builder; >>
- Конструктор служит для инициализации FirebaseApp с использованием нашего JSON-файла
- Метод sendByTopic() производит отправку уведомлений пользователям подписанным на заданную тему.
- Метод subscribeUsers() подписывет на тему (topic) пользователей (clientTokens).
Подводим итоги
По сути, библиотека Firebase собирает нам JSON примерно такого вида:
] length: 1 body: "как-то так" image: "https://habrastorage.org/webt/7i/k5/77/7ik577fzskgywduy_2mfauq1gxs.png" > priority: "normal">
А на стороне клиента Вы уже парсите его, как нравится.
Как отправлять веб-push-уведомления на Java
Узнайте, как подписаться на веб-push-уведомления в браузере и отправлять push-сообщения с сервера Java. Помеченный java, веб-разработчик, весенняя загрузка, машинопись.
Веб-push-уведомления – это способ информировать пользователей вашего приложения о том, что произошло что-то важное.
Пользователи могут получать веб-push-уведомления, даже если они не активно используют ваше приложение, например, если приложение открыто на фоновой вкладке или даже если оно не открыто.
Push-уведомления широко поддерживаются всеми браузерами, кроме Safari: 78% веб-пользователей используют браузер, который их поддерживает.
В этом уроке я покажу вам как подписаться на уведомления в браузере и как отправлять уведомления с Java-сервера .
Видеоверсия
Немного предыстории: как работают веб-push-уведомления
Веб-push-уведомления основаны на двух веб-стандартах: API уведомлений и Push API (который, в свою очередь, использует Service Worker ). Для их работы требуется протокол HTTPS.
Подписка на push-уведомления
- Сервер делится своим открытым ключом с браузером
- Браузер использует открытый ключ для подписки на push-сервис (у каждого браузера свой собственный).
- Служба push возвращает подписку с уникальным URL-адресом конечной точки, который можно использовать для отправки push-сообщений
- Подписка сохраняется на сервере
Отправка push-уведомлений
- Сервер подписывает заголовок авторизации своим закрытым ключом
- Сервер отправляет сообщение на уникальный URL-адрес конечной точки
- Push-сервер расшифровывает заголовок авторизации
- Push-сервер отправляет сообщение на устройство/браузер
Настройте проект и сгенерируйте ДЕЙСТВИТЕЛЬНЫЕ ключи
Я использую Vaadin Fusion для этого примера. Использование термоядерного синтеза Пружинный ботинок на задней панели и Горит на передней панели.
Здесь я расскажу только о ключевых шагах. Вы можете найти полный исходный код на ГитХаб .
Вы можете создать новый проект Fusion с помощью командной строки Vaadin:
npx @vaadin/cli init --fusion push-app
Создайте набор ПУСТЫХ ключей с помощью пакета web-push npm.
npx web-push generate-vapid-keys
Создайте новый файл .environment в каталоге проекта и используйте его для хранения ключей. Добавьте его в свой файл .gitignore чтобы вы случайно не опубликовали его.
export VAPID_PUBLIC_KEY=BAwZxXp0K. export VAPID_PRIVATE_KEY=1HLNMKEE.
Добавьте зависимость библиотеки Java Web Push в pom.xml :
nl.martijndwars web-push 5.1.1
Загрузите файл среды и запустите приложение:
Создайте службу Java для обработки подписок и отправки уведомлений
Создайте новую службу загрузки Spring, MessageService.java . Эта служба будет считывать в ключах и
package com.example.application; import java.io.IOException; import java.security.GeneralSecurityException; import java.security.Security; import java.time.LocalTime; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import javax.annotation.PostConstruct; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.jose4j.lang.JoseException; import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import nl.martijndwars.webpush.Notification; import nl.martijndwars.webpush.PushService; import nl.martijndwars.webpush.Subscription; @Service public class MessageService < @Value("$") private String publicKey; @Value("$") private String privateKey; private PushService pushService; private Listsubscriptions = new ArrayList<>(); @PostConstruct private void init() throws GeneralSecurityException < Security.addProvider(new BouncyCastleProvider()); pushService = new PushService(publicKey, privateKey); >public String getPublicKey() < return publicKey; >public void subscribe(Subscription subscription) < System.out.println("Subscribed to " + subscription.endpoint); this.subscriptions.add(subscription); >public void unsubscribe(String endpoint) < System.out.println("Unsubscribing from " + endpoint); subscriptions = subscriptions.stream().filter(s ->!endpoint.equals(s.endpoint)).collect(Collectors.toList()); > public void sendNotification(Subscription subscription, String messageJson) < try < pushService.send(new Notification(subscription, messageJson)); >catch (GeneralSecurityException | IOException | JoseException | ExecutionException | InterruptedException e) < e.printStackTrace(); >> @Scheduled(fixedRate = 15000) private void sendNotifications() < System.out.println("Sending notifications to all subscribers"); var json = """ < "title": "Server says hello!", "body": "It is now: %s" >"""; subscriptions.forEach(subscription -> < sendNotification(subscription, String.format(json, LocalTime.now())); >); > >
Некоторые ключевые моменты, на которые следует обратить внимание:
- Аннотация @Value(«$») считывает переменные среды в поля.
- Служба хранит подписки в Списке . В более практичном приложении вы бы сохранили их в базе данных вместе с пользователем.
- Вы отправляете push-уведомления с помощью push-сервиса.отправить(новое уведомление(подписка, Json сообщения)) . Полезная нагрузка также может быть обычным текстом, но JSON является более гибким.
- Сервис каждые 15 секунд рассылает всем абонентам уведомление, содержащее текущее время.
Создайте конечную точку для доступа к серверу
Далее вам нужен способ доступа к серверу из браузера. В Vaadin Fusion вы делаете это, определяя Конечную точку . Конечная точка будет генерировать типы машинописных текстов и методы доступа TS, которые вы можете использовать в клиентском коде.
package com.example.application; import com.vaadin.flow.server.connect.Endpoint; import com.vaadin.flow.server.connect.auth.AnonymousAllowed; import nl.martijndwars.webpush.Subscription; @Endpoint @AnonymousAllowed public class MessageEndpoint < private MessageService messageService; public MessageEndpoint(MessageService messageService) < this.messageService = messageService; >public String getPublicKey() < return messageService.getPublicKey(); >public void subscribe(Subscription subscription) < messageService.subscribe(subscription); >public void unsubscribe(String endpoint) < messageService.unsubscribe(endpoint); >>
Некоторые вещи, которые следует отметить:
- Конечные точки защищены по умолчанию. Вы можете разрешить анонимный доступ с помощью @AnonymousAllowed .
- Конечная точка внедряет службу сообщений и делегирует ей подписку и отказ от подписки.
Подписаться на уведомления в браузере
Создайте представление для подписки на уведомления. Компонент Освещенный элемент отслеживает две части состояния:
- разрешил ли пользователь уведомления
- есть ли у пользователя существующая push-подписка
import < customElement, html, state >from "lit-element"; import "@vaadin/vaadin-button"; import < View >from "../view"; import * as server from "Frontend/generated/MessageEndpoint"; @customElement("notifications-view") export class NotificationsView extends View < @state() denied = Notification.permission === "denied"; @state() subscribed = false; render() < return html`Web Push Notifications 📣
$ You have blocked notifications. You need to manually enable them in your browser. ` : ""> $Hooray! You are subscribed to receive notifications 🙌" theme="error">Unsubscribe ` : html`You are not yet subscribed to receive notifications.
" theme="primary">Subscribe `> `; > async firstUpdated() < const registration = await navigator.serviceWorker.getRegistration(); this.subscribed = !!(await registration?.pushManager.getSubscription()); >async subscribe() < const notificationPermission = await Notification.requestPermission(); if (notificationPermission === "granted") < const publicKey = await server.getPublicKey(); const registration = await navigator.serviceWorker.getRegistration(); const subscription = await registration?.pushManager.subscribe(< userVisibleOnly: true, applicationServerKey: this.urlB64ToUint8Array(publicKey), >); if (subscription) < this.subscribed = true; // Serialize keys uint8array ->base64 server.subscribe(JSON.parse(JSON.stringify(subscription))); > > else < this.denied = true; >> async unsubscribe() < const registration = await navigator.serviceWorker.getRegistration(); const subscription = await registration?.pushManager.getSubscription(); if (subscription) < await subscription.unsubscribe(); await server.unsubscribe(subscription.endpoint); this.subscribed = false; >> private urlB64ToUint8Array(base64String: string) < const padding = "=".repeat((4 - (base64String.length % 4)) % 4); const base64 = (base64String + padding) .replace(/\-/g, "+") .replace(/_/g, "/"); const rawData = window.atob(base64); const outputArray = new Uint8Array(rawData.length); for (let i = 0; i < rawData.length; ++i) < outputArray[i] = rawData.charCodeAt(i); >return outputArray; > >
Важной частью здесь является метод subscribe() -. Вот что он делает:
- Запрашивает у пользователя разрешение на показ уведомлений с помощью Notification.requestPermission() . Ответ будет “удовлетворен” или “отклонен”. записка: Если пользователь откажется, вы не сможете спросить его снова. Обязательно запрашивайте пользователя только тогда, когда он ожидает и хочет получать уведомления.
- Если пользователь дает разрешение, извлеките открытый ключ с сервера и используйте pushManager Service Worker для подписки на уведомления. Ключ Сервера приложений – это массив UINT8, содержащий открытый ключ. Вам нужно преобразовать его с помощью прилагаемого метода. (Не самый удобный API 🤷 ♂ ️)
- Если подписка завершится успешно, отправьте ее на сервер.
Обрабатывать входящие push-сообщения в Сервисном работнике
Как только вы подпишетесь на уведомления, сервер будет отправлять уведомления каждые 15 секунд.
Переопределите сотрудника службы, созданного Vaadin, скопировав цель/sw.ts -> интерфейс/sw.ts .
Добавьте следующих двух слушателей в sw.ts :
self.addEventListener("push", (e) => < const data = e.data?.json(); if (data) < self.registration.showNotification(data.title, < body: data.body, >); > >); self.addEventListener("notificationclick", (e) => < e.notification.close(); e.waitUntil(focusOrOpenWindow()); >); async function focusOrOpenWindow() < const url = new URL("/", self.location.origin).href; const allWindows = await self.clients.matchAll(< type: "window", >); const appWindow = allWindows.find((w) => w.url === url); if (appWindow) < return appWindow.focus(); >else < return self.clients.openWindow(url); >>
- Прослушиватель fetch вызывается при поступлении нового сообщения. Прочитайте свойство события данных как JSON, чтобы получить доступ к полезной нагрузке сообщения.
- Используйте самостоятельную регистрацию.showNotification() для отображения уведомления с использованием данных сообщения.
- Закройте уведомление.
- Посмотрите, есть ли у пользователя открытая вкладка “Приложение”. Если они это сделают, сосредоточьтесь на этом. Если они этого не сделают, откройте новое окно.
Исходный код
Читайте ещё по теме: