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

Matt Gaunt
Joe Medley
Joe Medley

当 Chrome 首次支持 Web Push API 时,它依赖于 Firebase Cloud Messaging (FCM)(以前称为 Google Cloud Messaging (GCM))推送服务。这需要使用其专有 API。这使得 Chrome 可以在 Web 推送协议规范仍在编写时为开发者提供 Web Push API,而后在 Web Push Protocol 缺少 Web Push Protocol 规范时提供身份验证(即消息发送者与其声称的身份相符)。好消息是,这两种情况都不是这样。

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

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

FCM 支持 Web 推送协议

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

现在,FCM 支持 Web 推送协议,您可以保留该端点,并将该网址用作 Web 推送协议端点。(这与 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 是“Voluntary Application Server Identification”(自愿应用服务器标识)的新别名。这个新规范本质上定义了应用服务器与推送服务之间的握手,并允许推送服务确认是哪个网站在发送消息。借助 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(),
    };
}

使用公钥进行订阅

如需使用 VAPID 公钥为 Chrome 用户订阅推送,您需要使用 subscribe() 方法的 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 发送消息,您需要发出包含两个额外 HTTP 标头(Authorization 标头和 Crypto-Key 标头)的常规 Web Push 协议请求。

授权标头

Authorization 标头是一个带有“WebPush”字样的已签名 JSON Web 令牌 (JWT)

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] 为准)。
  • 主题(“订阅”)
    • 主题必须是网址或 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 用作 Authorization 标头,并且名称前附加“WebPush”,具体如下所示:

WebPush eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiJ9.eyJhdWQiOiJodHRwczovL2ZjbS5nb29nbGVhcGlzLmNvbSIsImV4cCI6MTQ2NjY2ODU5NCwic3ViIjoibWFpbHRvOnNpbXBsZS1wdXNoLWRlbW9AZ2F1bnRmYWNlLmNvLnVrIn0.Ec0VR8dtf5qb8Fb5Wk91br-evfho9sZT6jBRuQwxVMFyK5S8bhOjk8kuxvilLqTBmDXJM5l3uVrVOQirSsjq0A

请注意关于这一点的一些事项。首先,Authorization 标头包含字面上的“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,并且需要向要返回的 FCM 端点添加 Authorization 标头。

VAPID 提供了一个偏离这些专有要求的过渡期。如果您实现了 VAPID,它将在所有支持 Web 推送的浏览器中运行。随着越来越多的浏览器支持 VAPID,您可以决定何时从清单中移除 gcm_sender_id