跳到主要内容

认识

2025年02月27日
柏拉文
越努力,越幸运

一、认识


在一个完备的 Web 推送系统中,服务端通过 web-push 模块生成全局性的 VAPID 密钥对并调用setVapidDetails 配置包含 JWT 签名及必要 HTTP 头的推送请求,自动实现基于 ECDHHKDFAES-GCMpayload 加密,从而将安全、符合 Web 推送协议的通知发送到所有订阅者,而客户端则先通过请求通知权限和注册 Service Worker 获取激活状态,再利用 PushManager 结合 Uint8Array 格式的VAPID 公钥生成订阅对象,并在推送事件中通过 waitUntil 延长生命周期、调用 showNotification 展示通知和 postMessage 实现跨上下文通信,共同构建了一套安全、透明且高效的实时消息传递体系。

web-push 认识: web-push 模块是实现服务端 Web 推送的关键工具,它将复杂的推送协议细节、加密流程以及身份验证机制封装到简单易用的 API 中。通过自动加密消息、内置 VAPID 支持和兼容旧版推送服务,web-push 帮助开发者构建安全、可靠的推送系统,使得跨浏览器的消息推送变得更加便捷和高效。1. web-push 简化消息发送, 开发者只需调用简单的 API,传入订阅信息和消息内容,无需手动构造复杂的 HTTP 请求或处理加密细节; 2. 自动加密, 模块内部自动完成消息 payload 的加密流程,确保所有数据在传输过程中都是安全的; 3. 内置 VAPID 支持, 只需提供 VAPID 密钥对,web-push 就能生成对应的 JWT,并将服务器身份信息嵌入到推送请求中,简化了身份验证过程; 4. 兼容性处理, 对于依赖于 GCM(现 FCM)的浏览器,web-push 提供了向后兼容的支持,确保推送消息在不同浏览器环境中都能正确传递。

使得服务端向浏览器推送消息通知工作流:

一、VAPID 签名:

  1. 服务端: 服务启动后, 调用 webPush.generateVAPIDKeys() 来生成全局的 VAPID.publicKeyVAPID.privateKey。调用 webPush.setVapidDetails 来设置全局的 VAPID 详细信息, 包括: Subject, 通常是一个 mailto: 邮箱地址或 URL,用于标服务器或应用; VAPID 公钥, 用于客户端加密订阅数据,并在推送请求中附带身份信息; VAPID 私钥, 用于对推送请求进行签名,证明推送请求的合法性。所有用户的推送通知都会使用这同一组 VAPID 证书进行身份验证和加密处理。并将生成的 VAPID.publicKey 下发到客户端。

  2. 客户端: 接收到 VAPID.publicKey, 格式为 base64, 将 base64 格式转化为 Unit8Array 格式, 以备后续使用。

二、客户端工作流:

  1. 订阅通知: 客户端通过 Notification.requestPermission 向用户请求通知权限, 用户同意接收通知后, 浏览器通过 Service WorkerPushManager 生成订阅对象,并将其发送到服务端。首先, 客户端需要注册 Service Worker。然后, 客户端通过 navigator.serviceWorker 获取到 ServiceWorkerContainer, 用于提供 ServiceWorker 的注册、移除、升级和通信的访问。ServiceWorkerContainer 有一个 ready 属性, 提供了一种将代码执行延迟到 Service Worker 处于活动状态的方法。它返回一个永远不会拒绝的 Promise,并且无限期地等待,直到与当前页面关联的 ServiceWorkerRegistration 具有活动的 worker。满足该条件后,它将使用 ServiceWorkerRegistration 解析, 最终会返回一个 ServiceWorkerRegistration 实例对象。ServiceWorkerRegistration 有一个 PushManager 对象实例, 支持订阅,获取活动订阅和访问推送权限状态。PushManager 调用 subscribe 并传入之前存储的 VAPID 公钥, 来订阅通知, 并且将返回的 PushSubscription 数据发送给服务端。

  2. 接收推送: 当服务端发送推送消息时,浏览器会唤醒已注册的 Service Worker,处理推送事件并显示通知或传递消息给页面。Service Worker 中注册了 push 事件和 notificationclick。当服务端发送推送消息后, push 事件会收到该消息, 在 event.waitUntil 回调中, 调用 self.registration.showNotification 来显示通知弹窗, 并且这时候可以通过 clients.matchAll 获取我们的客户端, 通过 postMessage 发送一条消息, 当我们客户端的主线程收到消息之后, 就可以做刷新之类的操作了。用户点击通知弹窗会触发 notificationclick, 这时候可以做相应的处理, 比如打开页面。

    • event.waitUtil: 接受一个 Promise, 通知浏览器, 在这个 Promise 解决之前,请不要将当前 Service Worker 实例终止。由于 Service Worker 是事件驱动的,waitUntil 通过延长事件的生命周期来保护异步操作,确保诸如资源缓存、消息处理和后台同步等任务能够完整执行。

三、服务端工作流:

  1. 接收订阅信息: 服务端收到客户端发送的订阅对象,并存储用于后续推送。

  2. 构造推送请求: 使用 web-push 模块构造 HTTP 请求,按照 Web 推送协议的要求:

    1. 消息加密: 如果需要传输 payloadweb-push 会自动使用 ECDH 密钥交换、HKDF 派生以及 AES-GCM 加密算法对数据进行加密,确保消息在传输过程中不被窃取或篡改。

    2. VAPID 签名: 模块利用服务端提供的 VAPID 密钥对生成 JWT,并将其附加到请求中,证明服务器的合法性。

    3. 构造 HTTP 请求: 包括必要的头信息(例如 TTLContent-EncodingAuthorization 等),确保推送服务器能够正确解析和转发消息。

  3. 发送消息: 推送请求被发送到用户订阅中指定的 endpoint,推送服务(如 FCMMozilla 的推送服务)将通过 webPush.sendNotification 消息传递到目标浏览器。

二、语法


const webpush = require('web-push');

const vapidKeys = webpush.generateVAPIDKeys();

webpush.setGCMAPIKey('<Your GCM API Key Here>');
webpush.setVapidDetails(
'mailto:example@yourdomain.org',
vapidKeys.publicKey,
vapidKeys.privateKey
);

const pushSubscription = {
endpoint: '.....',
keys: {
auth: '.....',
p256dh: '.....'
}
};

webpush.sendNotification(pushSubscription, 'Your Push Payload Text');