tonyiweb

网络推送交互会变得好用

原文链接: developers.google.com

Chrome浏览器开始支持网络推送api时,它使用的是一项以前被叫做Google云消息的推送服务(GCM)的技术,现在叫Firebase云消息(FCM)。这个使用的是它们自己的私有api,使得开发者在网络推送协议还在草拟的时候或者根本没有网络推送协议的时候就可以使用网络推送API。好消息是,不管怎么说,上面两种情况都是不对的。

FCM/GCM 和chrome浏览器现在支持标准的网络推送协议,同时推送者可以通过实现VAPID(Voluntary Application Server Identification,自主应用服务器标识,它是一种为站点和网页之间推送协议增加授权的开源标准。)来获得授权,而不再需要'gcm_send_id'。

本文中,我将先介绍一下怎么将你现有的服务端代码改造以便可以通过FCM来使用网络推送协议,然后,我将给你展示怎么在客户端和服务端实现VAPID。

FCM支持网络推送协议

我们先讲讲整个的背景。当你的web应用注册了一个推送任务,它将会把这个得到一个推送服务的URL。你的服务器将会通过web应用给你的用户发送一些数据。在chrome浏览器里,如果没有使用VAPID,那么需要推送服务的URL就是一个FCM的终结点URL(后面回讲到使用VAPID)。在FCM支持网络推送协议之前,你必须从URL中抽取出注册ID放在一个FCM API请求的header里。 比如,一个FCM终结点URL https://android.googleapis.com/gcm/send/ABCD1234, 包含一个注册ID'ABCD1234'。

现在由于FCM支持网络推送协议,你可以保持这个终结点URL不变,并且把它作为一个网络推送协议的终结点(这将使得chrome浏览器和火狐浏览器及其它浏览器保持一致)。

在我们着手研究VAPID之前,我们需要确保服务端能正确处理FCM终结点URL。下面是一个在节点里给推送服务发送请求的例子。注意,在FCM里,我们会将API key加入到请求的headers里面。对于其它推送服务,是不用这个操作的。在chrome 52之前的版本,android设备上的opera和三星浏览器上,你还需要将"gcm_sender_id"写在web应用的manifest.json里。 API key和sender ID是服务端用来检查是否需要给某个请求的用户发送消息。

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');  
  }  
});


记住,你不需要更新推送任务,只需要按照上面展示的一样去定义headers,因为这就是FCM/GCM API的变化。

介绍用于服务端身份标识的VAPID

VAPID是Voluntary Application Server Identification (https://tools.ietf.org/html/draft-thomson-webpush-vapid) 的简称。它是用来定义服务端和推送服务之间的握手协议,从而来判定那个服务端可以发送消息。通过VAPID,当你推送一个消息时,就可以避免使用FCM定义的方法,也不再需要一个Firebase项目,不再需要gcm_sender_id或者用于验证的header。

它的过程很简单:

  1. web应用服务端创建一个公有和私有的密钥对,公钥给web应用。

  2. 当用户选择接收推送时,将这个公钥放在订阅的参数里。

  3. 当web应用服务端推送一条消息时,将一个用公钥签名的JSON Web Token包含在其中。

具体来看看这些步骤

创建公钥和私钥对

看到加密就头痛,所以这里就把相关的不分放在这里,忽略掉VAPID公钥和私钥的格式

web应用服务端应该生成和维护一个用基于P-256曲线的椭圆曲线数字签名算法(ECSDA)签名的公钥和私钥对。

你也可以参考怎么实现这个签名过程(https://github.com/web-push-libs/web-push/)

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

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


用公钥订阅推送服务

你需要通过将公钥以Uint8Array的格式作为参数传到subscribe()方法,才可以实现让一个chrome浏览器用户完成使用VAPID公钥订阅推送服务。

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


注意:即使是一个FCM URL,使用Web推送服务,而不是FCM协议,服务器端也是能够支持任何推送服务的。

推送一个消息

要使用VAPID推送一条消息,你需要发送一个普通的基于web推送协议的请求,这个请求带有两个额外的http headers: 用于验证的header和加密密钥的header。

用于验证的header

这个用于验证的header是一个以‘WebPush’开头的签名过的JSON Web Token(JWT)。

JWT 提供了一种方法,使得推送者可以对它签名,而接收者可以通过验证签名来判定是否来自正确的推送者。 JWT的结构是以点作为分隔的3段加密字符串。

<JWTHeader>.<Payload>.<Signature>


JWT的header

JWT的header包含了用于签名的算法名和自己的类型名。 用于VAPID的JWT header必须是:

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


这些将会被用base64编码,并组成JWT的第一部分

有效内容

有效内容是另一个JSON对象,它包括一下内容:

  • Audience ("aud")

    • 这个推送服务的源(并不是网站的源)。在javascript里,你可以通过const audience = new URL(subscription.endpoint).origin 来获得这个audience 。
  • Expiration Time ("exp")

    • 这是请求将会被认为无效的时间秒数,必须在请求发起之后的24小时内(UTC时间)。
  • Subject ("sub")

    • 主题需要是一个URL或者是一个邮箱地址,它是用来提供联系方式的,以便推送服务可以联系到消息的推送者。

一个有效内容可能就像下面的一样:

{  
    "aud": "http://push-service.example.com",  
    "exp": Math.floor((Date.now() / 1000) + (12 * 60 * 60)),  
    "sub": "mailto: my-email@some-url.com"  
}


这个JSON对象被用base64编码之后组成JWT的第二部分。

签名

签名是将前面编码过的header和有效内容加上点,然后在用之前创建的私钥加密而成的。签名也应该追加到前面的有效内容后面,中间以点作为间隔。

我将不会给你展示一个示例, 因为有一个好用的工具可以做这些:https://jwt.io/#libraries-io

‘WebPush’放在签名之后JWT之前,然后整体作为验证header,就像下面这样:

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A


要注意的是,首先,这个验证header包含有'WebPush',并且在'WebPush'之后必须有一个空格,之后才是JWT。同时,要用点号来分隔JWT的header,有效内容和签名。

加密密钥header

和验证header一样,你必须把VAPID的公钥加密然后通过base64加密,然后前面加上'p256ecdsa='这一串。

p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo


当你用加密的数据推送一个通知时,已经用上了加密的header,要加上web应用的服务端的密钥,只需要在后面加上一个分号就可以,就像下面这样:

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S_k7zi7A1coR_sVjHmGrlvzYpAT1n4NPbioFlQkIrTNL8EH4V3ZZ4vJE;
p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaN


注意:这个分号本来是要使用逗号的,但是chrome 52之前的版本有问题,当使用逗号推送的时候,会被阻止掉,这个问题在chrome 53已经修复了,因此等稳定下来之后,你可以将分号改为逗号了。

这些变化有哪些用处呢

通过是使用VAPID,当你在chrome里使用推送服务时,不再需要登录GCM的账号了,而且可以用相同的代码在chrome浏览器和火狐浏览器实现用户订阅推送和推送消息给用户,而且都是遵循标准的。

你需要记住的是,当使用chrome 51或者更早的版本,Android设备上的Opera和三星浏览器时,仍然需要在清单文件里定义'gcm_sender_id'和添加验证header到FCM终结点URL里。

VAPID提供了一个逃离私有API束缚的捷径。使用VAPID可以使你在所有浏览器上都可以使用网络推送服务。随着越来越多的浏览器支持VAPID,你可以选择从你的清单文件里去掉gcm_sender_id了。

注意: 你可以从下面的网址找到所有的文档和一些关于网络推送的最佳实践的例子。 (https://developers.google.com/web/fundamentals/push-notifications)