Web 推送互操作性方面的优势

Matt Gaunt
Joe Medley
Joe Medley

Chrome 最初支持 Web Push API 时,就是依赖于 Firebase Cloud Messaging (FCM)(以前称为 Google Cloud Messaging (GCM))推送服务。这需要使用其专有 API。这样一来,Chrome 便可以在网络推送协议规范仍在编写时向开发者提供 Web Push API,并在稍后没有网络推送协议时提供身份验证(即消息发送者与其所声称的身份相符)。好消息:现在这两种情况都不再是这样。

FCM / GCM 和 Chrome 现在支持标准网络推送协议,而发件人身份验证可通过实现 VAPID 来实现,这意味着您的 Web 应用不再需要“gcm_sender_id”。

在本文中,我首先介绍如何将现有服务器代码转换为将 Web 推送协议与 FCM 搭配使用。接下来,我将介绍如何 在客户端和服务器代码中实现 VAPID

FCM 支持网络推送协议

我们先来了解一些背景信息。当您的 Web 应用注册推送订阅时,系统会向其提供推送服务的网址。您的服务器将使用此端点通过您的 Web 应用向用户发送数据。在 Chrome 中,如果您订阅的用户未使用 VAPID,您将获得一个 FCM 端点。(稍后我们会介绍 VAPID)。在 FCM 支持网络推送协议之前,您必须先从网址末尾提取 FCM 注册 ID,并将其放入标头中,然后才能发出 FCM API 请求。例如,FCM 端点 https://android.googleapis.com/gcm/send/ABCD1234 的注册 ID 为“ABCD1234”。

由于 FCM 支持网络推送协议,因此您可以保持端点不变,并将网址用作网络推送协议端点。(这使得它能够与 Firefox 以及未来的所有其他浏览器保持一致。)

在深入了解 VAPID 之前,我们需要确保服务器代码可正确处理 FCM 端点。以下示例展示了如何在 Node 中向推送服务发出请求。请注意,对于 FCM,我们将 API 密钥添加到了请求标头中。对于其他推送服务端点,不需要此信息。对于版本 52 之前的 Chrome、Opera Android 和三星浏览器,您仍需要在 Web 应用的 manifest.json 中添加“gcm_sender_id”。API 密钥和发送者 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');
    }
});

请注意,这是对 FCM / GCM 的 API 的更改,因此您无需更新订阅,只需更改服务器代码以定义标头即可,如上所述。

推出用于服务器识别的 VAPID

VAPID 是“自愿应用服务器标识”的炫酷新简称。这一新规范实质上定义了应用服务器与推送服务之间的握手,并允许推送服务确认哪个网站在发送消息。借助 VAPID,您可以省去发送推送消息时特定于 FCM 的步骤。您不再需要 Firebase 项目、gcm_sender_idAuthorization 标头。

操作过程非常简单:

  1. 应用服务器创建一个公钥/私钥对。公钥会被提供给您的 Web 应用。
  2. 当用户选择接收推送时,将公钥添加到 subscribe() 调用的 options 对象中。
  3. 当应用服务器发送推送消息时,请包含已签名的 JSON Web 令牌以及公钥。

我们来详细看一下这些步骤。

创建公钥/私钥对

我非常擅长加密,以下是规范中有关 VAPID 公钥/私钥格式的相关部分:

应用服务器应生成并维护一个签名密钥对,该密钥对可与 P-256 曲线上的椭圆曲线数字签名 (ECDSA) 结合使用。

您可以在 web-push 节点库中查看如何执行此操作:

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

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

使用公钥订阅

如需为 Chrome 用户订阅使用 VAPID 公钥的推送功能,您需要使用 subscription() 方法的 applicationServerKey 参数将公钥作为 Uint8Array 传递。

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

发送推送消息

如需使用 VAPID 发送消息,您需要发出一个普通的 Web 推送协议请求,其中包含两个额外的 HTTP 标头:一个 Authorization 标头和一个 Crypto-Key 标头。

授权标头

Authorization 标头是一个签名的 JSON 网络令牌 (JWT),其前面带有“WebPush”。

JWT 是一种与第二方共享 JSON 对象的方式,采用这种方式时,发送方可以对其进行签名,而接收方可以验证签名是否来自预期的发送方。JWT 的结构是三个加密字符串,它们之间用一个点连接。

<JWTHeader>.<Payload>.<Signature>

JWT 标头

JWT 标头包含用于签名的算法名称和令牌类型。对于 VAPID,必须满足以下条件:

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

然后,采用 base64 网址编码并形成 JWT 的第一部分。

载荷

载荷是另一个包含以下内容的 JSON 对象:

  • 受众群体(“aud”)
    • 这是推送服务的来源(不是您网站的来源)。 在 JavaScript 中,您可以执行以下操作来获取受众群体:const audience = new URL(subscription.endpoint).origin
  • 到期时间(“exp”)
    • 这是指请求被视为过期的秒数。此时间必须在发出请求后的 24 小时内(采用世界协调时间 (UTC))。
  • 主题(“sub”)
    • 主题必须是网址或 mailto: 网址。这可提供一个联系人,以防推送服务需要与消息发送者联系。

示例载荷可能如下所示:

{
    "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 的第二部分。

签名

签名是将编码标头和载荷与点联接,然后使用您之前创建的 VAPID 私钥对结果进行加密。结果本身应以一个点附加到标头。

我不会展示相关的代码示例,因为有很多库将采用标头和载荷 JSON 对象并为您生成此签名。

已签名的 JWT 用作授权标头,并在其前面加上“WebPush”,如下所示:

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

请注意这方面的一些事项。首先,授权标头包含单词“WebPush”,后跟一个空格,接着是 JWT。另请注意用点分隔 JWT 标头、载荷和签名。

Crypto-Key 标头

除了 Authorization 标头之外,您还必须以 base64 网址编码字符串的形式将 VAPID 公钥添加到 Crypto-Key 标头中,并在其前面加上 p256ecdsa=

p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaNndIo

您发送包含加密数据的通知时,您已在使用 Crypto-Key 标头。因此,如需添加应用服务器密钥,只需在添加上述内容前添加一个英文分号,如下所示:

dh=BGEw2wsHgLwzerjvnMTkbKrFRxdmwJ5S_k7zi7A1coR_sVjHmGrlvzYpAT1n4NPbioFlQkIrTNL8EH4V3ZZ4vJE;
p256ecdsa=BDd3_hVL9fZi9Ybo2UUzA284WG5FZR30_95YeZJsiApwXKpNcF1rRPF3foIiBHXRdJI2Qhumhf6_LFTeZaN

这些变化的实际情况

借助 VAPID,您无需再注册 GCM 的帐号即可在 Chrome 中使用推送功能。此外,您可以使用相同的代码路径在 Chrome 和 Firefox 中订阅用户及向用户发送消息。两者都符合相关标准。

需要注意的是,在 Chrome 51 及更低版本中,对于 Android 版 Opera 和三星浏览器,您仍然需要在 Web 应用清单中定义 gcm_sender_id,并且需要将 Authorization 标头添加到要返回的 FCM 端点中。

VAPID 可以很好地满足这些专有要求。如果您实现 VAPID,它可在所有支持 Web 推送的浏览器中运行。随着越来越多的浏览器支持 VAPID,您可以决定何时从清单中删除 gcm_sender_id