Рубрики
Без рубрики

Как отправлять веб-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

Загрузите файл среды и запустите приложение:

source .env
mvn

Создайте службу 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("${vapid.public.key}")
  private String publicKey;
  @Value("${vapid.private.key}")
  private String privateKey;

  private PushService pushService;
  private List subscriptions = 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 📣

${this.denied ? html` You have blocked notifications. You need to manually enable them in your browser. ` : ""} ${this.subscribed ? html`

Hooray! You are subscribed to receive notifications 🙌

Unsubscribe ` : html`

You are not yet subscribed to receive notifications.

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() -. Вот что он делает:

  1. Запрашивает у пользователя разрешение на показ уведомлений с помощью Notification.requestPermission() . Ответ будет “удовлетворен” или “отклонен”. записка: Если пользователь откажется, вы не сможете спросить его снова. Обязательно запрашивайте пользователя только тогда, когда он ожидает и хочет получать уведомления.
  2. Если пользователь дает разрешение, извлеките открытый ключ с сервера и используйте pushManager Service Worker для подписки на уведомления. Ключ Сервера приложений – это массив UINT8, содержащий открытый ключ. Вам нужно преобразовать его с помощью прилагаемого метода. (Не самый удобный API 🤷 ♂ ️)
  3. Если подписка завершится успешно, отправьте ее на сервер.

Обрабатывать входящие 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() для отображения уведомления с использованием данных сообщения.
  • Прослушиватель щелчок уведомления вызывается при нажатии на уведомление.

    • Закройте уведомление.
    • Посмотрите, есть ли у пользователя открытая вкладка “Приложение”. Если они это сделают, сосредоточьтесь на этом. Если они этого не сделают, откройте новое окно.

Исходный код

Вы можете найти полный исходный код на моем GitHub: https://github.com/marcushellberg/fusion-push-notifications .

Оригинал: “https://dev.to/marcushellberg/how-to-send-web-push-notifications-in-java-21lo”