minishlink/web-push

PHP 的 Web Push 库

v9.0.1 2024-07-30 12:24 UTC

README

PHP 的 Web Push 库

Build Status

WebPush 可以用来向端点发送 Web Push 协议中描述的消息。

然后,浏览器会接收到这个推送消息,并可以使用 service workerNotifications API 创建通知。

需求

PHP 8.1+ 以及以下扩展

  • bcmath 和/或 gmp(可选,但性能更好)
  • mbstring
  • curl
  • openssl(具有椭圆曲线支持)

不支持和维护旧版本的 PHP,但是您可以使用以下兼容版本

  • PHP 5.6 或 HHVM: v1.x
  • PHP 7.0: v2.x
  • PHP 7.1: v3.x-v5.x
  • PHP 7.2: v6.x
  • PHP 7.3 7.4: v7.x
  • PHP 8.0 / 无椭圆曲线支持的 Openssl: v8.x

本 README 只与最新版本兼容。每个库版本都有一个 git 标签,可以读取相应的 README。

安装

使用 composer 下载并安装库及其依赖项。

composer require minishlink/web-push

用法

示例

可以在以下位置找到使用 web-push-php 的完整示例,其中包含 html+JS 前端和 php 后端:Minishlink/web-push-php-example

发送推送消息

<?php

use Minishlink\WebPush\WebPush;
use Minishlink\WebPush\Subscription;

// store the client-side `PushSubscription` object (calling `.toJSON` on it) as-is and then create a WebPush\Subscription from it
$subscription = Subscription::create(json_decode($clientSidePushSubscriptionJSON, true));

// array of notifications
$notifications = [
    [
        'subscription' => $subscription,
        'payload' => '{"message":"Hello World!"}',
    ], [
          // current PushSubscription format (browsers might change this in the future)
          'subscription' => Subscription::create([
              "endpoint" => "https://example.com/other/endpoint/of/another/vendor/abcdef...",
              "keys" => [
                  'p256dh' => '(stringOf88Chars)',
                  'auth' => '(stringOf24Chars)'
              ],
          ]),
          'payload' => '{"message":"Hello World!"}',
    ], [
        // old Firefox PushSubscription format
        'subscription' => Subscription::create([
            'endpoint' => 'https://updates.push.services.mozilla.com/push/abc...', // Firefox 43+,
            'publicKey' => 'BPcMbnWQL5GOYX/5LKZXT6sLmHiMsJSiEvIFvfcDvX7IZ9qqtq68onpTPEYmyxSQNiH7UD/98AUcQ12kBoxz/0s=', // base 64 encoded, should be 88 chars
            'authToken' => 'CxVX6QsVToEGEcjfYPqXQw==', // base 64 encoded, should be 24 chars
        ]),
        'payload' => 'hello !',
    ], [
        // old Chrome PushSubscription format
        'subscription' => Subscription::create([
            'endpoint' => 'https://fcm.googleapis.com/fcm/send/abcdef...',
        ]),
        'payload' => null,
    ], [
        // old PushSubscription format
        'subscription' => Subscription::create([
            'endpoint' => 'https://example.com/other/endpoint/of/another/vendor/abcdef...',
            'publicKey' => '(stringOf88Chars)',
            'authToken' => '(stringOf24Chars)',
            'contentEncoding' => 'aesgcm', // one of PushManager.supportedContentEncodings
        ]),
        'payload' => '{"message":"test"}',
    ]
];

$webPush = new WebPush();

// send multiple notifications with payload
foreach ($notifications as $notification) {
    $webPush->queueNotification(
        $notification['subscription'],
        $notification['payload'] // optional (defaults null)
    );
}

/**
 * Check sent results
 * @var MessageSentReport $report
 */
foreach ($webPush->flush() as $report) {
    $endpoint = $report->getRequest()->getUri()->__toString();

    if ($report->isSuccess()) {
        echo "[v] Message sent successfully for subscription {$endpoint}.";
    } else {
        echo "[x] Message failed to sent for subscription {$endpoint}: {$report->getReason()}";
    }
}

/**
 * send one notification and flush directly
 * @var MessageSentReport $report
 */
$report = $webPush->sendOneNotification(
    $notifications[0]['subscription'],
    $notifications[0]['payload'], // optional (defaults null)
);

身份验证 (VAPID)

浏览器需要验证您的身份。一种称为 VAPID 的标准可以用于所有浏览器进行身份验证。您需要为您的服务器创建并提供公钥和私钥。这些密钥必须安全存储,并且不应更改。

您可以在实例化 WebPush 时指定您的身份验证详细信息。密钥可以直接传递(推荐),或者您可以加载 PEM 文件或其内容

<?php

use Minishlink\WebPush\WebPush;

$endpoint = 'https://fcm.googleapis.com/fcm/send/abcdef...'; // Chrome

$auth = [
    'VAPID' => [
        'subject' => 'mailto:me@website.com', // can be a mailto: or your website address
        'publicKey' => '~88 chars', // (recommended) uncompressed public key P-256 encoded in Base64-URL
        'privateKey' => '~44 chars', // (recommended) in fact the secret multiplier of the private key encoded in Base64-URL
        'pemFile' => 'path/to/pem', // if you have a PEM file and can link to it on your filesystem
        'pem' => 'pemFileContent', // if you have a PEM file and want to hardcode its content
    ],
];

$webPush = new WebPush($auth);
$webPush->queueNotification(...);

要生成未压缩的公钥和私钥,这些密钥以 Base64 编码,请在您的 Linux bash 中输入以下内容:

$ openssl ecparam -genkey -name prime256v1 -out private_key.pem
$ openssl ec -in private_key.pem -pubout -outform DER|tail -c 65|base64|tr -d '=' |tr '/+' '_-' >> public_key.txt
$ openssl ec -in private_key.pem -outform DER|tail -c +8|head -c 32|base64|tr -d '=' |tr '/+' '_-' >> private_key.txt

如果您无法访问 Linux bash,可以打印 createVapidKeys 函数的输出

var_dump(VAPID::createVapidKeys()); // store the keys afterwards

在客户端,不要忘记使用 VAPID 公钥作为 applicationServerKey 进行订阅:(urlBase64ToUint8Array 来源 这里

serviceWorkerRegistration.pushManager.subscribe({
    userVisibleOnly: true,
    applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
})

重用 VAPID 头部

VAPID 头部使用 JSON Web Token (JWT) 验证您的身份。该令牌的有效负载包括订阅中包含的端点的协议和主机名以及过期时间戳(通常在 12-24 小时之间),并使用您的公钥和私钥进行签名。鉴于这一点,发送到同一推送服务的两个通知将使用相同的令牌,因此您可以使用 setDefaultOptions() 或在构造函数中更改默认选项来重用它们以提升性能

$webPush->setReuseVAPIDHeaders(true);

通知和默认选项

每个通知都可以有特定的生存时间、紧急程度和主题。WebPush 标准声明 紧急程度 是可选的,但有些用户报告说,当未指定时,Safari 会抛出错误。这可能在将来得到修复。您可以使用 setDefaultOptions() 或在构造函数中更改默认选项来更改默认选项

<?php

use Minishlink\WebPush\WebPush;

$defaultOptions = [
    'TTL' => 300, // defaults to 4 weeks
    'urgency' => 'normal', // protocol defaults to "normal". (very-low, low, normal, or high)
    'topic' => 'newEvent', // not defined by default. Max. 32 characters from the URL or filename-safe Base64 characters sets
    'batchSize' => 200, // defaults to 1000
];

// for every notification
$webPush = new WebPush([], $defaultOptions);
$webPush->setDefaultOptions($defaultOptions);

// or for one notification
$webPush->sendOneNotification($subscription, $payload, ['TTL' => 5000]);

TTL

生存时间 (TTL,以秒为单位) 是推送服务(例如 Mozilla)保留推送消息的时间长度,以防用户浏览器尚未可访问(例如尚未连接)。您可能希望对重要的通知使用很长的时间。默认的 TTL 为 4 周。然而,如果您发送多个非必要通知,请将 TTL 设置为 0:只有当用户当前连接时,推送通知才会被投递。对于其他情况,如果您的用户有多个时区,您应该使用至少一天的时间,如果他们没有,几个小时就足够了。

紧急程度

紧急程度可以是 "非常低"、"低"、"正常" 或 "高"。如果浏览器供应商实现了此功能,它将在移动设备上节省电池寿命(参见 协议)。

主题

此字符串将使供应商只向用户显示此主题的最后一个通知(参见 协议)。

batchSize

如果您一次发送数万条通知,由于 Guzzle 中端点的调用方式,您可能会因内存溢出而受到影响。为了解决这个问题,WebPush 以批量形式发送通知。默认大小为 1000。根据您的服务器配置(内存),您可能想降低这个数字。在实例化 WebPush 或调用 setDefaultOptions 时执行此操作。或者,如果您想为特定的刷新自定义此参数,将其作为参数提供:$webPush->flush($batchSize)

服务器错误

您可以看到浏览器供应商的服务器在遇到错误时发送回什么(推送订阅过期、参数错误...)。

<?php

/** @var \Minishlink\WebPush\MessageSentReport $report */
foreach ($webPush->flush() as $report) {
    $endpoint = $report->getEndpoint();

    if ($report->isSuccess()) {
        echo "[v] Message sent successfully for subscription {$endpoint}.";
    } else {
        echo "[x] Message failed to sent for subscription {$endpoint}: {$report->getReason()}";

        // also available (to get more info)

        /** @var \Psr\Http\Message\RequestInterface $requestToPushService */
        $requestToPushService = $report->getRequest();

        /** @var \Psr\Http\Message\ResponseInterface $responseOfPushService */
        $responseOfPushService = $report->getResponse();

        /** @var string $failReason */
        $failReason = $report->getReason();

        /** @var bool $isTheEndpointWrongOrExpired */
        $isTheEndpointWrongOrExpired = $report->isSubscriptionExpired();
    }
}

请注意:您只能遍历 一次 \Generator 对象。

Firefox 错误在 autopush 文档 中列出。

有效载荷长度、安全性和性能

库将有效载荷加密。理论上的最大有效载荷长度为 4078 字节(或 ASCII 字符)。由于 兼容性原因(存档),您的有效载荷长度应小于 3052 字节。

库默认填充有效载荷。这更安全,但会降低您的服务器和用户设备上的性能。

为什么更安全?

当您加密一定长度的字符串时,结果字符串的长度始终相同,无论您加密多少次初始字符串。这可以使攻击者猜测有效载荷的内容。为了绕过这一点,此库在初始有效载荷中添加了一些空填充,以便所有加密过程的输入都具有相同的长度。这样,所有加密过程的输出也将具有相同的长度,攻击者将无法猜测您的有效载荷内容。

为什么它会降低性能?

加密更多字节需要更多运行时间,并且也会使用户的设备在解密时变慢。此外,发送和接收数据包将花费更多时间。这对有有限数据计划的用户也不友好。

如何禁用或自定义自动填充?

您可以通过自定义自动填充来更好地满足您的需求。

以下是一些设置想法

  • (默认) Encryption::MAX_COMPATIBILITY_PAYLOAD_LENGTH (2820字节) 以与 Android Firefox 兼容(见 #108
  • Encryption::MAX_PAYLOAD_LENGTH (4078字节) 以实现最大安全性
  • false 以实现最大性能
  • 如果您知道您的有效载荷不会超过 X 字节,那么将其设置为 X 以在安全性和性能之间取得最佳平衡。
<?php

use Minishlink\WebPush\WebPush;

$webPush = new WebPush();
$webPush->setAutomaticPadding(false); // disable automatic padding
$webPush->setAutomaticPadding(512); // enable automatic padding to 512 bytes (you should make sure that your payload is less than 512 bytes, or else an attacker could guess the content)
$webPush->setAutomaticPadding(true); // enable automatic padding to default maximum compatibility length

自定义 HTTP 客户端

WebPush 使用 Guzzle。它将使用它找到的最合适的客户端,大多数情况下将是 MultiCurl,这允许并行发送多个通知。

您可以在实例化 WebPush 时自定义默认请求选项和超时。

<?php

use Minishlink\WebPush\WebPush;

$timeout = 20; // seconds
$clientOptions = [
    \GuzzleHttp\RequestOptions::ALLOW_REDIRECTS => false,
]; // see \GuzzleHttp\RequestOptions
$webPush = new WebPush([], [], $timeout, $clientOptions);

常见问题(FAQ)

有没有为我的 favorite PHP 框架提供的插件/包/扩展?

以下是可以用的

请随意添加您自己的!

关于安全性?

有效载荷根据 Web Push 消息加密 标准加密,使用用户公钥和您可以通过遵循 Web Push API 规范获得的身份验证密钥。

内部,WebPush 使用 WebToken 框架和 OpenSSL 来处理加密密钥生成和加密。

我该如何扩展?

以下是一些建议

  1. 确保您的服务器上安装了 MultiCurl
  2. 在安全性和性能之间找到满足您需求的最佳平衡(见上文)
  3. 找到合适的批量大小(在 defaultOptions 中设置或在 flush() 的参数中设置)
  4. 使用 flushPooled() 而不是 flush()。前者使用并发请求,加速处理过程并通常将请求速度加倍。

如何解决 "SSL 证书问题:无法获取本地颁发者证书"?

您的安装缺少一些证书。

  1. 下载 cacert.pem
  2. 编辑您的 php.ini:在 [curl] 之后,输入 curl.cainfo = /path/to/cacert.pem

您还可以强制使用不带对等方验证的客户端。

如何解决 "找不到类 'Minishlink\WebPush\WebPush' " 的问题?

请确保包含 Composer 的 自动加载器

require __DIR__ . '/path/to/vendor/autoload.php';

我在发送通知时遇到认证错误

请确保数据库字段足够大,以容纳您要存储的数据长度(#233)。对于端点,用户报告说 URL 长度不超过 500 个字符,但这一情况可能会变化,因此您可以将其设置为大多数浏览器允许的 2048 个字符限制。

我丢失了我的 VAPID 密钥!

问题 #58

我正在使用 Google Cloud Messaging (GCM),我该如何使用这个库?

此服务已不再存在。它已于 2019 年 5 月 29 日被 Google 的 Firebase Cloud Messaging (FCM) 取代。

我正在使用 Firebase Cloud Messaging (FCM),我该如何使用这个库?

本库不支持Firebase云消息服务(FCM)。旧的Chrome订阅(2018年之前和VAPID)确实使用了Firebase云消息服务(FCM)的旧版HTTP协议,该协议自2023年起已被弃用,并将于2024年6月停止工作。已移除对此过时订阅的支持。

请注意,旧版HTTP协议和带有VAPID的Web Push使用相同的端点URL

https://fcm.googleapis.com/fcm/send

带有VAPID的Web Push将在此URL上继续可用。目前无需采取任何进一步行动。

如何发送数据?

浏览器厂商不允许在不创建通知的情况下使用Push API发送数据。请使用一些替代API,如WebSocket/WebTransport或后台同步。

我需要向原生应用发送通知。(例如iOS的APNS)

WebPush用于Web应用。您需要类似RMSPushNotificationsBundle(Symfony)的东西。

这是PHP...我需要JavaScript!

本库受到了Node.js web-push-libs/web-push 库的启发。

参考

示例、注释和概述

互联网工程任务组(IETF)

  • 使用HTTP推送进行通用事件传递RFC8030
  • Web推送的消息加密RFC8291
  • Web推送的自愿应用服务器标识(VAPID)RFC8292

万维网联盟(W3C)

许可

MIT