Победа в совместимости Web Push

Когда Chrome впервые поддержал Web Push API, он полагался на службу push-уведомлений Firebase Cloud Messaging (FCM), ранее известную как Google Cloud Messaging (GCM). Для этого потребовалось использовать собственный API. Это позволило Chrome сделать API Web Push доступным для разработчиков в то время, когда спецификация протокола Web Push еще писалась, а затем обеспечить аутентификацию (то есть отправителем сообщения является тот, кем он себя называет) в то время, когда протокол Web Push не хватало этого. Хорошие новости: ни то, ни другое больше не соответствует действительности.

FCM/GCM и Chrome теперь поддерживают стандартный протокол Web Push , а аутентификация отправителя может быть достигнута путем внедрения VAPID , что означает, что вашему веб-приложению больше не нужен gcm_sender_id.

В этой статье я собираюсь сначала описать, как преобразовать существующий серверный код для использования протокола Web Push с FCM. Далее я покажу вам, как реализовать VAPID как в клиентском, так и в серверном коде.

FCM поддерживает протокол Web Push

Начнем с небольшого контекста. Когда ваше веб-приложение регистрируется для принудительной подписки, ему предоставляется URL-адрес службы принудительной отправки. Ваш сервер будет использовать эту конечную точку для отправки данных вашему пользователю через ваше веб-приложение. В Chrome вам будет предоставлена ​​конечная точка FCM, если вы подпишетесь на пользователя без VAPID. (Мы рассмотрим VAPID позже). До того, как FCM поддерживал протокол Web Push, вам приходилось извлекать идентификатор регистрации FCM из конца URL-адреса и помещать его в заголовок перед отправкой запроса API FCM. Например, конечная точка FCM https://android.googleapis.com/gcm/send/ABCD1234 будет иметь идентификатор регистрации «ABCD1234».

Теперь, когда FCM поддерживает протокол Web Push, вы можете оставить конечную точку нетронутой и использовать URL-адрес в качестве конечной точки протокола Web Push. (Это приводит его в соответствие с Firefox и, надеюсь, с любым другим браузером будущего.)

Прежде чем мы углубимся в VAPID, нам необходимо убедиться, что код нашего сервера правильно обрабатывает конечную точку FCM. Ниже приведен пример запроса к push-сервису в Node. Обратите внимание, что для FCM мы добавляем ключ API в заголовки запросов. Для других конечных точек службы push-уведомлений это не понадобится. Для Chrome до версии 52, Opera Android и браузера Samsung вам также необходимо включить gcm_sender_id в файл манифеста вашего веб-приложения.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 — это новое короткое название для « добровольной идентификации сервера приложений ». Эта новая спецификация по существу определяет рукопожатие между вашим сервером приложений и службой push-уведомлений и позволяет службе push-уведомлений подтверждать, какой сайт отправляет сообщения. С помощью VAPID вы можете избежать специфических для FCM действий по отправке push-сообщения. Вам больше не нужен проект Firebase, gcm_sender_id или заголовок Authorization .

Процесс довольно прост:

  1. Ваш сервер приложений создает пару открытого/закрытого ключей. Открытый ключ передается вашему веб-приложению.
  2. Когда пользователь решает получать push-уведомления, добавьте открытый ключ к объекту параметров вызова subscribe().
  3. Когда ваш сервер приложений отправляет push-сообщение, включите подписанный веб-токен JSON вместе с открытым ключом.

Давайте рассмотрим эти шаги подробно.

Создайте пару открытого/закрытого ключей.

Я плохо разбираюсь в шифровании, поэтому вот соответствующий раздел спецификации, касающийся формата открытых/закрытых ключей VAPID:

Серверы приложений ДОЛЖНЫ генерировать и поддерживать пару ключей подписи, которую можно использовать с цифровой подписью на основе эллиптической кривой (ECDSA) по кривой P-256.

Вы можете увидеть, как это сделать, в библиотеке узлов web-push :

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

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

Подписка с открытым ключом

Чтобы подписаться на push-уведомление пользователя 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

Отправка push-сообщения

Чтобы отправить сообщение с использованием VAPID, вам необходимо выполнить обычный запрос по протоколу Web Push с двумя дополнительными заголовками HTTP: заголовком авторизации и заголовком крипто-ключа.

Заголовок авторизации

Заголовок Authorization — это подписанный веб-токен JSON (JWT) с надписью WebPush перед ним.

JWT — это способ поделиться объектом JSON со второй стороной таким образом, чтобы отправляющая сторона могла его подписать, а получающая сторона могла убедиться, что подпись принадлежит ожидаемому отправителю. Структура JWT представляет собой три зашифрованные строки, соединенные одной точкой.

<JWTHeader>.<Payload>.<Signature>

JWT-заголовок

Заголовок JWT содержит имя алгоритма, используемого для подписи, и тип токена. Для VAPID это должно быть:

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

Затем этот URL-адрес кодируется в формате Base64 и образует первую часть JWT.

Полезная нагрузка

Полезная нагрузка — это еще один объект JSON, содержащий следующее:

  • Аудитория («ауд»)
    • Это источник службы push-уведомлений ( НЕ источник вашего сайта). В JavaScript вы можете сделать следующее, чтобы получить аудиторию: const audience = new URL(subscription.endpoint).origin
  • Срок действия («exp»)
    • Это количество секунд, в течение которых запрос должен считаться истекшим. Это ДОЛЖНО быть сделано в течение 24 часов с момента подачи запроса по всемирному координированному времени (UTC).
  • Тема («подраздел»)
    • Тема должна быть URL-адресом или URL-адресом mailto: Это обеспечивает точку контакта на случай, если службе push-уведомлений потребуется связаться с отправителем сообщения.

Пример полезной нагрузки может выглядеть следующим образом:

{
    "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. Также обратите внимание на точки, разделяющие заголовок JWT, полезные данные и подпись.

Заголовок крипто-ключа

Помимо заголовка авторизации, вы должны добавить свой открытый ключ 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 в манифесте вашего веб-приложения, и вам нужно будет добавить заголовок авторизации в конечную точку FCM, которая будет возвращен.

VAPID обеспечивает отход от этих проприетарных требований. Если вы реализуете VAPID, он будет работать во всех браузерах, поддерживающих веб-push. Поскольку все больше браузеров поддерживают VAPID, вы можете решить, когда удалить gcm_sender_id из вашего манифеста.