关于浏览器 Web Push 原理的详细解释

这是一份关于浏览器 Web Push 原理的详细讲解,内容涵盖了其核心概念、工作流程、安全机制、实现细节以及最佳实践,力求全面且深入,篇幅将超过三千字。


浏览器 Web Push 原理深度解析:从用户许可到消息送达的全链路剖析

Web Push(网页推送)技术是现代 Web 应用中一项至关重要的功能。它允许网站(Web 应用)向其用户发送可操作的通知消息,即使用户当前没有打开该网站的标签页,甚至在浏览器未运行的情况下(在某些操作系统上)。这项能力极大地增强了 Web 应用的用户粘性(Re-engagement),使其在功能上更接近原生应用(Native App),是构建渐进式 Web 应用(PWA)的核心技术之一。

要彻底理解 Web Push 的原理,我们需要将其分解为几个关键部分:核心参与者、工作流程、安全机制(VAPID 与加密)以及具体的实现细节

一、 核心概念与参与者

Web Push 的整个生态系统由以下几个关键角色构成,理解它们各自的职责是理解整个流程的基础。

  1. 用户代理 (User Agent):通常指用户的浏览器(如 Chrome, Firefox, Edge 等)。它是用户与 Web Push 系统交互的直接媒介,负责请求用户授权、管理 Service Worker、从推送服务接收消息,并最终向用户展示通知。

  2. 应用服务器 (Application Server):这是你的网站后端。它的职责是存储用户的订阅信息,并在需要时(例如,有新文章发布、有新私信等)构建推送消息,然后向推送服务发起请求,要求其将消息传递给指定的用户。

  3. 推送服务 (Push Service):这是一个由浏览器厂商提供和维护的中间件服务。例如,Google 的 Firebase Cloud Messaging (FCM)、Mozilla 的 autopush 服务等。它的作用像一个高度可靠的邮局。它从你的应用服务器接收推送请求,负责将消息可靠地、高效地、安全地传递给正确的用户浏览器。应用服务器不需要知道用户的 IP 地址或设备状态,只需与这个稳定的推送服务对话即可。

  4. Service Worker:这是一个在浏览器后台运行的JavaScript 脚本,独立于网页主线程。它是实现 Web Push 的技术基石。即使在用户关闭了网站标签页后,Service Worker 依然可以被推送服务唤醒,以接收并处理推送消息(例如,显示一个通知)。没有 Service Worker,离线推送和后台消息处理就无从谈起。

  5. 推送订阅 (Push Subscription):这是一个JSON 对象,包含了将消息推送到特定用户所需的所有信息。它本质上是用户的“推送地址”。其中最关键的信息是 endpoint(推送服务的 URL)和用于加密的公钥 keys。每个订阅对象都唯一地对应一个用户的特定浏览器和设备。

二、 完整工作流程:从订阅到通知的八个步骤

Web Push 的完整生命周期可以分为两个主要阶段:订阅阶段推送阶段。下面我们通过一个详细的八步流程来拆解它。

阶段一:订阅(用户授权与信息交换)

步骤 1:请求用户授权 (Permission Request)

一切始于用户许可。Web 应用不能擅自发送通知,必须首先明确征得用户的同意。

  • 触发时机:最佳实践不是在用户一进入页面时就弹出授权请求,这会显得非常突兀和令人反感。而是在用户执行了某个有意义的操作后(例如,点击了“订阅更新”按钮),或者在界面上清晰地解释了订阅的好处之后,再通过 JavaScript 调用 Notification.requestPermission()

  • 用户交互:浏览器会弹出一个标准化的对话框,询问用户是否允许该网站显示通知。用户可以选择“允许”(granted)、“拒绝”(denied)或关闭对话框(default,效果等同于拒绝)。

  • 结果处理Notification.requestPermission() 返回一个 Promise,解析后的值为用户的选择('granted', 'denied', 'default')。只有当值为 'granted' 时,才能继续后续步骤。

步骤 2:注册 Service Worker

如果用户授权,下一步就是确保有一个 Service Worker 处于活动状态,准备好接收未来的推送。

  • 注册代码:在你的主应用 JavaScript 中,你需要检查浏览器是否支持 Service Worker,然后进行注册。

    
    if ('serviceWorker' in navigator) {
      navigator.serviceWorker.register('/service-worker.js')
        .then(registration => {
          console.log('Service Worker 注册成功:', registration);
          // 注册成功后,进行下一步:订阅推送
        })
        .catch(error => {
          console.error('Service Worker 注册失败:', error);
        });
    }
    
  • service-worker.js:这是一个独立的 JS 文件,它将被下载并安装在浏览器后台。在订阅阶段,它可能还是一个空文件,但必须存在。

步骤 3:获取推送订阅 (Push Subscription)

一旦 Service Worker 注册成功,我们就可以通过其 PushManager 接口来向推送服务发起订阅。

  • 订阅调用

    
    navigator.serviceWorker.ready.then(registration => {
      const applicationServerKey = urlBase64ToUint8Array('YOUR_VAPID_PUBLIC_KEY'); // VAPID公钥
      registration.pushManager.subscribe({
        userVisibleOnly: true, // 必须为 true,表示每次推送都会有用户可见的通知
        applicationServerKey: applicationServerKey
      })
      .then(subscription => {
        console.log('用户订阅成功:', subscription);
        // 将订阅对象发送到你的应用服务器
        sendSubscriptionToServer(subscription);
      })
      .catch(error => {
        console.error('用户订阅失败:', error);
      });
    });
    
  • 关键参数

    • userVisibleOnly: true:这是一个强制性要求,旨在防止开发者在用户不知情的情况下进行静默推送,保护用户隐私。

    • applicationServerKey:这是 VAPID 公钥(后文详述),用于验证你的应用服务器的身份。你需要将一个 Base64 编码的公钥转换为 Uint8Array 格式。

  • 订阅对象:如果成功,subscribe() 方法会返回一个 PushSubscription 对象。它看起来像这样:

    
    {
      "endpoint": "https://fcm.googleapis.com/fcm/send/c2R..._wU",
      "expirationTime": null,
      "keys": {
        "p256dh": "BEl...8A",
        "auth": "O5...9w"
      }
    }
    
    • endpoint:推送服务的 URL,你的后端将向这个地址发送消息。

    • keys.p256dh:基于 P-256 椭圆曲线的 Diffie-Hellman 公钥,用于加密推送消息。

    • keys.auth:一个身份验证密钥,与 p256dh 配合用于加密。

步骤 4:将订阅对象发送到应用服务器

浏览器已经拿到了用户的“推送地址”(订阅对象),现在必须将其发送给你的应用服务器进行存储,以便日后使用。

  • 实现方式:通常使用 fetch API 发送一个 POST 请求到你的后端 API。

    
    function sendSubscriptionToServer(subscription) {
      fetch('/api/save-subscription', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(subscription)
      });
    }
    
  • 后端处理:你的应用服务器(例如用 Node.js, Python, Java 编写)接收到这个 JSON 对象后,应将其与相应的用户信息关联起来,并存储在数据库中(如 MySQL, MongoDB, Redis 等)。

至此,订阅阶段完成。你的数据库中已经有了可以向该用户发送推送的凭证。

阶段二:推送(服务器触发与消息送达)

步骤 5:应用服务器触发推送

当有事件发生(如发布新博客),你的应用服务器决定向一个或多个用户发送推送。

  • 准备工作:服务器从数据库中取出目标用户的订阅对象。

  • 构建请求:服务器需要向订阅对象中的 endpoint URL 发起一个 HTTP POST 请求。这个请求需要包含:

    1. 消息体 (Payload):你想要发送的实际数据,例如 {"title": "新文章发布!", "body": "快来看看我们的最新内容!"}。这个消息体必须被加密。

    2. 特定的 HTTP 头部

      • TTL (Time To Live):告诉推送服务这条消息的有效期(秒)。如果在此期间无法送达用户设备(例如设备离线),消息将被丢弃。

      • Authorization:包含一个使用 VAPID 私钥签名的 JWT (JSON Web Token),用于向推送服务证明“我就是这个订阅所关联的应用服务器”。

      • Content-Type, Content-Encoding 等描述加密内容的头部。

步骤 6:推送服务处理并转发消息

浏览器厂商的推送服务接收到你的请求后,会执行以下操作:

  1. 验证身份:检查 Authorization 头部中的 VAPID签名是否有效。如果无效,则拒绝请求。

  2. 定位设备:根据 endpoint 找到目标用户设备。推送服务维护着从 endpoint 到实际设备网络地址的内部映射。

  3. 唤醒设备/浏览器:如果设备处于休眠状态,推送服务会通过操作系统级别的推送机制(如 APNs for Apple, FCM for Android)发送一个轻量级的“唤醒”信号。

  4. 传递消息:一旦设备上的浏览器准备就绪,推送服务就将加密的推送消息传递给它。

步骤 7:Service Worker 接收推送事件

浏览器接收到消息后,会立即唤醒对应的 Service Worker(即使网站标签页已关闭),并在其上下文中触发一个 push 事件。

  • 监听事件:你需要在 service-worker.js 文件中设置一个 push 事件的监听器。

    
    // service-worker.js
    self.addEventListener('push', event => {
      console.log('接收到推送事件:', event);
      const data = event.data ? event.data.json() : { title: '默认标题', body: '默认内容' };
      
      const title = data.title;
      const options = {
        body: data.body,
        icon: '/images/icon.png',
        badge: '/images/badge.png' // 用于移动设备状态栏的小图标
      };
    
      // 阻止事件的默认处理,并显示我们自己的通知
      event.waitUntil(
        self.registration.showNotification(title, options)
      );
    });
    
  • 数据解密event.data 对象包含了从服务器发送过来的负载。浏览器会自动使用订阅时生成的密钥对数据进行解密,所以你在 Service Worker 中可以直接通过 event.data.json()event.data.text() 来获取明文数据。

  • event.waitUntil():这个方法会延长 Service Worker 的生命周期,直到传入的 Promise 完成。我们必须用它来包裹 showNotification,以确保在 Service Worker 终止前,通知已经成功显示。

步骤 8:显示通知与用户交互

self.registration.showNotification(title, options) 是最终向用户展示通知的 API。

  • 用户看到通知:操作系统会以标准方式(如桌面右下角弹窗、手机顶部通知栏)显示这个通知。

  • 处理点击事件:仅仅显示通知是不够的,我们通常希望用户点击通知后能回到我们的网站。这需要监听 notificationclick 事件。

    
    // service-worker.js
    self.addEventListener('notificationclick', event => {
      event.notification.close(); // 关闭通知
    
      // 打开一个新的窗口或聚焦到已有的窗口
      event.waitUntil(
        clients.openWindow('https://your-website.com/some-page')
      );
    });
    

三、 核心安全机制:VAPID 与端到端加密

Web Push 的设计非常注重安全和隐私,主要体过了两个方面:

1. VAPID (Voluntary Application Server Identification)

  • 目的:解决“身份认证”问题。推送服务如何知道是合法的应用服务器在发送消息,而不是某个恶意方?在 VAPID 出现之前,开发者通常需要去特定厂商(如 Google)的控制台注册,获取一个专有的 API 密钥。VAPID 提供了一种开放、标准的、跨浏览器的方式。

  • 原理

    1. 密钥对生成:应用服务器生成一对公钥/私钥(基于 P-256 椭圆曲线)。

    2. 公钥共享:在步骤 3 的订阅阶段,应用将自己的公钥applicationServerKey)传递给浏览器,浏览器再将其与订阅信息一起发送给推送服务。推送服务就将这个公钥与该订阅绑定。

    3. 请求签名:在步骤 5 的推送阶段,应用服务器使用其私钥对一个包含其身份信息(如来源 origin)和过期时间的 JWT 进行签名。

    4. 签名验证:推送服务收到请求后,用之前存储的公钥来验证 JWT 签名的合法性。如果验证通过,就证明请求确实来自拥有相应私钥的服务器,从而确认了身份。

2. 负载加密 (Payload Encryption)

  • 目的:解决“内容保密”问题。推送服务作为一个中间人,它不应该能够读取你发送给用户的消息内容。这实现了真正的端到端加密(从你的应用服务器到用户的浏览器)。

  • 原理

    1. 密钥来源:加密所用的密钥并非由你的服务器单方面决定。它利用了在步骤 3 订阅时,由用户浏览器生成的密钥对(keys.p256dhkeys.auth)。

    2. 加密过程:你的应用服务器在发送消息前,会使用标准的 Message Encryption for Web Push 协议(基于 ECE - Elliptic Curve Encryption)进行加密。这个过程大致是:

      • 服务器也生成一个临时的椭圆曲线密钥对。

      • 利用服务器的临时私钥和用户的公钥(p256dh)通过椭圆曲线迪菲-赫尔曼密钥交换(ECDH)算法,计算出一个共享密钥。

      • 结合用户的 auth 密钥和这个共享密钥,派生出最终的加密密钥和 nonce。

      • 使用这个最终密钥(通常是 AES-128-GCM 算法)来加密你的消息负载。

    3. 解密过程:当用户的浏览器接收到加密的消息后,它会执行一个逆向的过程。因为它拥有自己的私钥(与 p256dh 公钥配对),所以它能与服务器的临时公钥(包含在加密消息的头部)计算出相同的共享密钥,从而解密消息内容。

由于这个加密过程比较复杂,实际开发中我们通常会使用现成的库(如 Node.js 的 web-push 库)来自动处理 VAPID 签名和负载加密,开发者只需提供 VAPID 密钥对和订阅对象即可。

四、 总结与最佳实践

总结 Web Push 原理:

Web Push 是一个基于发布-订阅模型的系统。前端通过 Service WorkerPush API 向浏览器厂商的推送服务进行订阅,获得一个唯一的订阅对象(地址),并将其发送给应用服务器存储。当需要推送时,应用服务器使用 VAPID 密钥对自身进行签名,并使用用户的公钥对消息进行端到端加密,然后向推送服务发起请求。推送服务验证签名后,负责将加密消息可靠地送达用户浏览器,唤醒 Service Worker 来解密数据并最终通过 Notification API 显示通知。

最佳实践:

  1. 友好的授权请求:不要在页面加载时立即请求权限。先向用户解释订阅的好处,在用户产生明确意图后再触发请求。

  2. 提供管理界面:给用户一个简单的方式来取消订阅或管理通知偏好。

  3. 发送高质量内容:推送通知是一种打扰。确保你发送的每一条消息都是及时的、相关的、有价值的。滥用会导致用户取消订阅。

  4. 处理订阅过期/失效:推送服务在无法送达时会返回特定的 HTTP 状态码(如 404 Not Found410 Gone)。你的后端需要捕获这些响应,并从数据库中删除相应的无效订阅,避免资源浪费。

  5. 利用通知操作 (Actions)showNotification API 允许你添加操作按钮,这能提供更丰富的交互,例如“立即查看”和“稍后提醒”。

  6. 注意数据负载大小:推送服务的负载大小有限制(通常是几 KB),所以只发送关键数据,详情内容让用户点击通知后到网页上查看。

通过深入理解上述原理和流程,开发者可以更有效地利用 Web Push 技术,在不依赖原生应用的情况下,构建出能够主动、及时触达用户的强大 Web 应用。