Progressive Web Apps(PWA)核心技术-使用Firebase Cloud Messaging实现推送通知

罗星洲
2023-12-01

Chrome目前使用Firebase云消息传递(FCM)作为其推送服务。 FCM最近采用了Web Push协议。 FCM是Google云消息传递(GCM)的后续产品,支持相同的功能和更多功能。

要使用Firebase云消息传递,您需要在Firebase上设置项目(请参阅VAPID部分以绕过此步骤)。 大致流程如下:

1、在Firebase控制台中,选择创建新项目。
2、提供项目名称,然后单击创建项目。
3、单击导航面板中项目名称旁边的“设置”图标,然后选择“项目设置”。
4、打开云消息传递选项卡。 您可以在此页面找到您的服务器密钥和发件人ID。 保存这些值。
为了将FCM邮件路由到正确的service worker,需要知道发件人ID。 通过将gcm_sender_id属性添加到应用程序的manifest.json文件来提供此功能。 例如:

{
  "name": "Push Notifications app",
  "gcm_sender_id": "370072803732"
}

要让FCM将没有负载的通知推送到您的Web客户端,请求必须包含以下内容:

  • 订阅端点URL
  • 公共服务器密钥。 FCM使用它来检查发出请求的服务器是否允许向接收用户发送消息。
    生产站点或应用程序通常会设置一个服务,以让服务器与FCM进行交互。

我们可以使用cURL在我们的应用程序中测试推送消息。 我们可以向推送服务发送一个名为“tickle”的空消息,然后推送服务向浏览器发送消息。 如果通知显示,那么我们已经做了一切正确的,我们的应用程序已准备好从服务器推送消息。

发送请求到FCM发出推送消息的cURL命令:

curl "ENDPOINT_URL" --request POST --header "TTL: 60" --header "Content-Length: 0" \
--header "Authorization: key=SERVER_KEY"

例如:

curl "https://android.googleapis.com/gcm/send/fYFVeJQJ2CY:APA91bGrFGRmy-sY6NaF8a...gls7HZcwJL4 \ 
LFxjg0y0-ksEhKjpeFC5P" --request POST --header "TTL: 60" --header "Content-Length: 0" \
 --header "Authorization: key=AIzaSyD1JcZ8WM1vTtH6Y0tXq_Pnuw4jgj_92yg"

向用户推送消息相对比较容易。 但是,到目前为止,我们发送的通知都是空的。 Chrome和Firefox支持使用推送消息将数据传送给service worker的功能。
我们先来看看service worker需要进行哪些更改才能将数据从推送消息中提取出来。

self.addEventListener('push', function(e) {
  var body;

  if (e.data) {
    body = e.data.text();
  } else {
    body = 'Push message no payload';
  }

  var options = {
    body: body,
    icon: 'images/notification-flat.png',
    vibrate: [100, 50, 100],
    data: {
      dateOfArrival: Date.now(),
      primaryKey: 1
    },
    actions: [
      {action: 'explore', title: 'Explore this new world',
        icon: 'images/checkmark.png'},
      {action: 'close', title: 'I don't want any of this',
        icon: 'images/xmark.png'},
    ]
  };
  e.waitUntil(
    self.registration.showNotification('Push Notification', options)
  );
});

当我们收到带有数据的推送通知时,数据直接在事件对象上可用。 这些数据可以是任何类型,json,blob,array或text。

服务端推送
以nodejs为例:
安装推送服务:npm install web-push

var webPush = require('web-push');

var pushSubscription = {"endpoint":"https://android.googleapis.com/gcm/send/f1LsxkKphfQ:APA91bFUx7ja4BK4JVrNgVjpg1cs9lGSGI6IMNL4mQ3Xe6mDGxvt_C_gItKYJI9CAx5i_Ss6cmDxdWZoLyhS2RJhkcv7LeE6hkiOsK6oBzbyifvKCdUYU7ADIRBiYNxIVpLIYeZ8kq_A",
"keys":{"p256dh":"BLc4xRzKlKORKWlbdgFaBrrPK3ydWAHo4M0gs0i1oEKgPpWC5cW8OCzVrOQRv-1npXRWk8udnW3oYhIO4475rds=", "auth":"5I2Bu2oKdyy9CwL8QVF0NQ=="}};

var payload = 'Here is a payload!';

var options = {
  gcmAPIKey: 'AIzaSyD1JcZ8WM1vTtH6Y0tXq_Pnuw4jgj_92yg',
  TTL: 60
};

webPush.sendNotification(
  pushSubscription,
  payload,
  options
);

本示例将订阅对象,有效内容和服务器密钥传递给sendNotification方法。 它还传递一个生存时间,这是以秒为单位的值,它描述了推送服务保留推送消息的时间(默认为四周)。

通过VAPID身份验证识别您的服务
Web推送协议旨在通过保持用户匿名性,并且不需要您的应用程序和推送服务之间的高度认证来尊重用户的隐私。这提出了一些挑战:

  • 未经认证的推送服务面临更大的攻击风险
  • 拥有端点的任何应用程序服务器都能够向用户发送消息
  • 如果出现问题,推送服务无法与开发者联系
    解决方案是让发布者使用“自愿应用服务器标识”(VAPID)Web推送协议。这至少为应用程序服务器提供了一个稳定的身份,这也可以包括联系信息,例如电子邮件地址。

该规范列出了使用VAPID的几个好处:

  • 推送服务可以使用一致的身份为应用程序服务器建立期望行为。然后可以使用明显偏离既定标准来触发异常处理程序。
  • 在特殊情况下,自愿提供的联系信息可用于联系应用服务器运营商。
  • 部署推送服务的经验表明,软件错误或异常情况会导致推送消息量大幅增加。联系应用程序服务器的运营商已被证明是有价值的。
  • 即使没有可用的联系信息,在选择是否丢弃推送消息时,具有良好声誉的应用程序服务器可能优先于未识别的应用程序服务器。
  • 使用VAPID还可以避免发送推送消息的FCM特定步骤。您不再需要Firebase项目,gcm_sender_id或Authorization标头。

使用VAPID
这个过程非常简单:

您的应用程序服务器创建一个公钥/私钥对。 公钥是给你的web app。
当用户选择接收推送时,将公钥添加到subscribe()调用options对象。
当您的服务器发送推送消息时,请将签名的JSON Web Token与公钥一起包含在内。

创建一个公钥/私钥对
以下是规范中有关VAPID公钥/私钥格式的相关部分:

应用程序服务器应该在P-256曲线上生成并维护一个可用于椭圆曲线数字签名(ECDSA)的签名密钥对。
可以借助web-push node library完成这部分:

function generateVAPIDKeys() {  
  const vapidKeys = webpush.generateVAPIDKeys();
  return {
    publicKey: vapidKeys.publicKey,  
    privateKey: vapidKeys.privateKey,  
  };  
}

订阅公钥
要订阅一个Chrome用户使用VAPID公钥进行推送,请使用subscribe()方法的applicationServerKey参数将公钥作为Uint8Array传递。

const publicKey = new Uint8Array([0x4,0x37,0x77,0xfe,....]);
serviceWorkerRegistration.pushManager.subscribe(
  {
     userVisibleOnly:true,
     applicationServerKey:publicKey
  }
);

您将通过检查生成的订阅对象中的端点来了解它是否工作正常; 如果来源是fcm.googleapis.com,则表示正在运行。

注意:尽管这是一个FCM URL,但是使用Web推送协议而不是FCM协议,这样您的服务器端代码将适用于任何推送服务。
发送推送信息

要使用VAPID发送消息,可以使用另外两个HTTP标头进行正常的Web推送协议请求:Authorization header和JWT header。

  • Authorization header

授权标头是一个带有“WebPush”的签名JSON Web Token(JWT)。

构成格式为:
<JWTHeader>.<Payload>.<Signature>

JWT header:

{  
  "typ": "JWT",  //类型
  "alg": "ES256"  //签名算法名称
}

Payload:(这个JSON对象是base64 url编码)

{  
    "aud": "http://push-service.example.com",  //可以通过const audience = new URL(subscription.endpoint).origin获得
    "exp": Math.floor((Date.now() / 1000) + (12 * 60 * 60)),  //过期时间
    "sub": "mailto: my-email@some-url.com"  //可以是一个联络的url或mailto
}

Signature
这个签名是由编码过后的header和payload用点连接然后与之前创建的VAPID生成的私钥加密而来的。 签名库
签名的JWT被用作授权头,“WebPush”作为前缀,如下所示:

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

这里有几点需要指出。 首先,授权标题字面上包含单词WebPush,后面跟着一个空格,然后是JWT。 还要注意分隔JWT header,payload和signature的点。

Crypto-Key
与授权标题一样,您必须将您的VAPID公钥添加到加密密钥标头中,作为base64 url编码的字符串,其前缀为p256ecdsa =。

p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo

当您使用加密数据发送通知时,您已经使用Crypto-Key header,因此要添加应用程序服务器密钥,只需在添加上述内容之前添加逗号,如下:

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S_k7zi7A1coR_sVjHmGrlvzYpAT1n4NPbioFlQkIrTNL8EH4V3ZZ4vJE,
p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaN

在版本52之前的Chrome中存在一个缺陷,在Crypto-key header中需要使用分号而不是逗号。

cURL发送推送消息:

curl "https://updates.push.services.mozilla.com/wpush/v1/gAAAAABXmk....dyR" --request POST --header "TTL: 60" --header "Content-Length: 0" --header "Authorization: WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A" --header "Crypto-Key: p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo"

nodejs发送推送消息:

var webPush = require('web-push');

var pushSubscription = {"endpoint":"https://fcm.googleapis.com/fcm/send/c0NI73v1E0Y:APA91bEN7z2weTCpJmcS-MFyfbgjtmlAWuV5YaaNw625_Rq2-f0ZrVLdRPXKGm7B3uwfygicoCeEoWQxCKIxlL3RWG2xkHs6C8-H_cxq-4Z-isAiZ3ixo84-2HeXB9eUvkfNO_t1jd5s","keys":{"p256dh":"BHxSHtYS0q3i0Tb3Ni6chC132ZDPd5uI4r-exy1KsevRqHJvOM5hNX-M83zgYjp-1kdirHv0Elhjw6Hivw1Be5M=","auth":"4a3vf9MjR9CtPSHLHcsLzQ=="}};

var vapidPublicKey = 'BAdXhdGDgXJeJadxabiFhmlTyF17HrCsfyIj3XEhg1j-RmT2wXU3lHiBqPSKSotvtfejZlAaPywJ9E-7AxXQBj4
';
var vapidPrivateKey = 'VCgMIYe2BnuNA4iCfR94hA6pLPT3u3ES1n1xOTrmyLw
';

var payload = 'Here is a payload!';

var options = {
  vapidDetails: {
    subject: 'mailto:example_email@example.com',
    publicKey: vapidPublicKey,
    privateKey: vapidPrivateKey
  },
  TTL: 60
};

webPush.sendNotification(
  pushSubscription,
  payload,
  options
);
 类似资料: