承接 mojahed/webpush 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

mojahed/webpush

Composer 安装命令:

composer require mojahed/webpush

包简介

Server-side Web Push (VAPID + RFC 8291 aes128gcm) for PHP 7.2+. Framework-agnostic, zero third-party dependencies. Bundles the mds-webpush client JS.

README 文档

README

Server-side Web Push (VAPID + RFC 8291 aes128gcm) for PHP 7.2+.
Framework-agnostic. Zero third-party dependencies. Bundles the mds-webpush client JS.

Table of Contents

Requirements

Requirement Details
PHP 7.2 or later
ext-openssl Required (signing + AES-128-GCM encryption)
ext-curl Recommended for HTTP transport (stream fallback is used otherwise)
ext-gmp Recommended for ECDH on PHP 7.2 (bcmath is used as fallback)
ext-bcmath Alternative ECDH fallback when gmp is unavailable

On PHP 7.3+ with a standard openssl build, only ext-openssl is needed.

Installation

composer require mojahed/webpush

Generating VAPID Keys

VAPID keys identify your server to the push service (FCM, Mozilla, etc.). Generate them once and store them securely.

npx web-push generate-vapid-keys

Output:

Public Key:  BNcRdreALRFXTkOATJDP...
Private Key: UxLevFltuo37CIQR...

Store in your .env (Laravel) or equivalent:

VAPID_PUBLIC_KEY=BNcRdreALRFXTkOATJDP...
VAPID_PRIVATE_KEY=UxLevFltuo37CIQR...
VAPID_SUBJECT=mailto:you@yourdomain.com

The subject must be a mailto: address or an https: URL. It lets the push service contact you if there is a problem.

Quick Start

use Mojahed\WebPush;
use Mojahed\Subscription;

// 1. Create the sender
$push = new WebPush([
    'vapid' => [
        'subject'    => 'mailto:you@example.com',
        'publicKey'  => $_ENV['VAPID_PUBLIC_KEY'],
        'privateKey' => $_ENV['VAPID_PRIVATE_KEY'],
    ],
]);

// 2. Build a subscription from the JSON your browser posted
$subscription = Subscription::fromJson($rawJson);
// — or from an array:
$subscription = Subscription::fromArray([
    'endpoint' => 'https://fcm.googleapis.com/fcm/send/...',
    'keys'     => ['p256dh' => '...', 'auth' => '...'],
]);

// 3. Send a notification
$result = $push->send($subscription, [
    'title' => 'Hello',
    'body'  => 'Your order has shipped.',
    'icon'  => '/images/icon-192.png',
    'url'   => '/orders/42',
]);

if ($result['success']) {
    // HTTP 201 — delivered to the push service
}

if ($result['expired']) {
    // HTTP 404 or 410 — delete this subscription from your database
}

PHP API Reference

WebPush

new WebPush(array $config)
Config Key Type Default Description
vapid.subject string mailto: or https: sender identifier (required)
vapid.publicKey string VAPID public key, base64url (required)
vapid.privateKey string VAPID private key, base64url (required)
ttl int 2419200 Message Time-To-Live in seconds (max 28 days)
urgency string null One of very-low, low, normal, high
timeout int 30 HTTP request timeout in seconds

send()

$result = $push->send(
    $subscription,          // Subscription, array, or JSON string
    $payload,               // array (JSON-encoded), string, or null
    $options                // optional per-message overrides: ttl, urgency, topic
);

Per-message $options:

Key Type Description
ttl int Overrides the instance default TTL
urgency string Overrides the instance default urgency
topic string Collapse key — same topic replaces a queued notification (max 32 chars)

quickSend() — static convenience

$result = WebPush::quickSend(
    ['vapid' => [...]], // same config array as the constructor
    $subscription,
    $payload,
    $options
);

Subscription

Accepts the standard browser subscription shape or a flat array.

// From raw JSON (e.g. request body from the browser)
$sub = Subscription::fromJson($request->getContent());

// From an associative array
$sub = Subscription::fromArray([
    'endpoint' => 'https://...',
    'keys'     => ['p256dh' => '...', 'auth' => '...'],
]);

// Flat form (keys at top level, not nested)
$sub = Subscription::fromArray([
    'endpoint' => 'https://...',
    'p256dh'   => '...',
    'auth'     => '...',
]);

// Construct directly with raw bytes or base64url strings
$sub = new Subscription($endpoint, $p256dh, $auth);

Keys may be supplied as base64url strings (as the browser provides) or as raw bytes — both are accepted and normalized to raw bytes internally for both p256dh and auth.

Response Array

send() returns an associative array:

Key Type Description
success bool true when the push service returned HTTP 201
status int Raw HTTP status code
body string Response body from the push service
expired bool true on 404 or 410 — subscription should be deleted
rateLimited bool true on 429 — back off and retry
retryAfter int|null Seconds to wait before retrying (from Retry-After header)
endpoint string The subscription endpoint that was targeted
$result = $push->send($subscription, $payload);

if ($result['expired']) {
    // Remove from DB — this subscription will never work again
    $subscription->delete();
}

if ($result['rateLimited']) {
    $wait = $result['retryAfter'] ?? 60;
    // Re-queue the job to run after $wait seconds
}

Client-Side Setup (mds.webpush.js)

The webpush-js/ directory contains the browser-side library. No build step, no dependencies — drop it in and use it.

Files

File Purpose Where to place
mds.webpush.js Main library — load in your page /public/js/ or CDN
mds.webpush.sw.js Service Worker /public/ (must be at domain root)

Important: mds.webpush.sw.js must be served from your domain root (/mds.webpush.sw.js) so its scope covers the entire site. Never place it in a subdirectory.

Register

Call MdsWebpush.register() from a user gesture (button click). Browsers block permission prompts that fire automatically on page load.

<meta name="csrf-token" content="{{ csrf_token() }}">
<script src="/js/mds.webpush.js"></script>

<button id="notify-btn">Enable Notifications</button>

<script>
document.getElementById('notify-btn').addEventListener('click', function () {
    MdsWebpush.register({
        vapidPublicKey: 'YOUR_VAPID_PUBLIC_KEY',
        subscribeUrl:   '/mds-push/subscribe',
        csrfToken:      document.querySelector('meta[name="csrf-token"]').content,
    });
});
</script>

What happens internally:

  1. Validates config and checks HTTPS + browser support.
  2. If permission is already granted, registers the Service Worker silently.
  3. If permission is default, checks whether enough time has passed since the user last dismissed the popup (controlled by retryAfter).
  4. Shows a customizable trust popup. If the user clicks Allow, triggers the browser permission prompt.
  5. On permission granted, subscribes via PushManager and POSTs the subscription to subscribeUrl — but only if the subscription is new or has changed, to avoid hitting your server on every page load.

Unsubscribe

MdsWebpush.unsubscribe({
    vapidPublicKey: 'YOUR_VAPID_PUBLIC_KEY',
    unsubscribeUrl: '/mds-push/unsubscribe', // optional — notifies your server
    csrfToken:      document.querySelector('meta[name="csrf-token"]').content,
});
// Returns a Promise<boolean>

This calls subscription.unsubscribe() in the browser, optionally POSTs the old endpoint to unsubscribeUrl, and clears all local state.

Config Reference

Required

Key Type Description
vapidPublicKey string Your VAPID public key (base64url)

URLs

Key Type Default Description
swUrl string /mds.webpush.sw.js Path to the Service Worker file
subscribeUrl string /mds-push/subscribe Server endpoint that saves the subscription
unsubscribeUrl string|null null Server endpoint called on unsubscribe() — optional
csrfToken string|null null CSRF token (required for Laravel and similar frameworks)

Popup

Key Type Default
popup.title string 'Stay Informed'
popup.message string 'Enable notifications to receive important updates instantly.'
popup.okText string 'Allow Notifications'
popup.cancelText string 'Not Now'

Behavior

Key Type Default Description
retryAfter string 'daily' When to re-show the popup after a dismiss: 'daily', 'weekly', 'never'
onError function console.error Called with (code, message) on any error

Error Codes

Available as MdsWebpush.ERROR_CODES.*:

Code When
HTTPS_REQUIRED Page is not served over HTTPS (localhost is exempt)
SW_NOT_SUPPORTED Browser does not support Service Workers
PUSH_NOT_SUPPORTED Browser does not support the Push API
PERMISSION_DENIED User denied the notification permission
SW_REGISTRATION_FAILED Service Worker failed to register
SUBSCRIBE_FAILED PushManager.subscribe() failed
UNSUBSCRIBE_FAILED unsubscribe() could not complete
SERVER_FAILED Server returned an error when saving the subscription
INVALID_CONFIG vapidPublicKey is missing or invalid
MdsWebpush.register({
    vapidPublicKey: '...',
    onError: function (code, message) {
        if (code === MdsWebpush.ERROR_CODES.PERMISSION_DENIED) {
            // Show UI explaining how to re-enable from browser settings
        }
    },
});

Push Payload Fields

Your server sends this JSON as the notification payload. All fields are optional.

{
    "title":     "New Message",
    "body":      "You have a new message from Ahmed.",
    "icon":      "/images/icon-192.png",
    "badge":     "/images/badge-72.png",
    "image":     "/images/preview.jpg",
    "url":       "/messages",
    "tag":       "message-thread-1",
    "renotify":  true
}
Field Type Description
title string Notification title. Defaults to 'Notification' if omitted
body string Notification body text
icon string Small icon URL (192×192 recommended)
badge string Monochrome badge icon for Android status bar (72×72)
image string Large image shown inside the notification
url string URL opened when the user clicks the notification. Defaults to /
tag string Collapse key — a new notification with the same tag replaces the previous one
renotify bool Re-alert the user when a same-tagged notification replaces a previous one

Payload size limit: ~4 079 bytes (RFC 8291 single-record limit).

Laravel Integration

Migration

Schema::create('push_subscriptions', function (Blueprint $table) {
    $table->id();
    $table->foreignId('user_id')->constrained()->cascadeOnDelete();
    $table->text('endpoint');
    $table->string('p256dh');
    $table->string('auth');
    $table->timestamps();

    $table->unique(['user_id', 'endpoint'], 'unique_user_endpoint');
});

Routes & Controller

// routes/web.php
Route::middleware('auth')->group(function () {
    Route::post('/mds-push/subscribe',   [PushController::class, 'subscribe']);
    Route::post('/mds-push/unsubscribe', [PushController::class, 'unsubscribe']);
});
// app/Http/Controllers/PushController.php
use App\Models\PushSubscription;
use Illuminate\Http\Request;

class PushController extends Controller
{
    public function subscribe(Request $request)
    {
        PushSubscription::updateOrCreate(
            [
                'user_id'  => auth()->id(),
                'endpoint' => $request->input('endpoint'),
            ],
            [
                'p256dh' => $request->input('keys.p256dh'),
                'auth'   => $request->input('keys.auth'),
            ]
        );

        return response()->json(['success' => true]);
    }

    public function unsubscribe(Request $request)
    {
        PushSubscription::where('user_id', auth()->id())
            ->where('endpoint', $request->input('endpoint'))
            ->delete();

        return response()->json(['success' => true]);
    }
}

Sending a Notification

use Mojahed\WebPush;
use App\Models\PushSubscription;

$push = new WebPush([
    'vapid' => [
        'subject'    => config('services.vapid.subject'),
        'publicKey'  => config('services.vapid.public_key'),
        'privateKey' => config('services.vapid.private_key'),
    ],
]);

$subscriptions = PushSubscription::where('user_id', $user->id)->get();

foreach ($subscriptions as $row) {
    $result = $push->send(
        [
            'endpoint' => $row->endpoint,
            'keys'     => ['p256dh' => $row->p256dh, 'auth' => $row->auth],
        ],
        [
            'title' => 'Order Shipped',
            'body'  => 'Your order #' . $order->id . ' is on its way.',
            'icon'  => '/images/icon-192.png',
            'url'   => '/orders/' . $order->id,
        ]
    );

    if ($result['expired']) {
        $row->delete(); // push service says this endpoint is gone
    }
}

Add these to config/services.php:

'vapid' => [
    'subject'     => env('VAPID_SUBJECT'),
    'public_key'  => env('VAPID_PUBLIC_KEY'),
    'private_key' => env('VAPID_PRIVATE_KEY'),
],

Cleaning Up Expired Subscriptions

Always delete subscriptions that come back expired — HTTP 404 and 410 both mean the push service has permanently invalidated the endpoint.

if ($result['expired']) {
    PushSubscription::where('endpoint', $result['endpoint'])->delete();
}

How It Works

Subscription flow

User clicks button
  → MdsWebpush.register(config)
      → validate vapidPublicKey
      → HTTPS + browser support checks
      → If permission granted → register SW silently → subscribe
      → If permission denied  → fire onError(PERMISSION_DENIED)
      → If permission default:
          → Check localStorage cancel date vs retryAfter
          → Show trust popup
              → "Not Now"  → store cancel date → stop
              → "Allow"    → browser permission prompt
                  → Denied  → store cancel date → fire onError
                  → Granted → register SW → subscribe
                      → POST to server only if subscription is new/changed

Push delivery flow

Server sends push
  → POST encrypted payload to push service endpoint (Google / Mozilla)
      → Push service delivers to browser
          → Browser wakes Service Worker
              → SW receives 'push' event
                  → SW calls showNotification()
                      → User sees notification
                          → User clicks notification
                              → SW focuses existing tab or opens new one

Automatic resubscription

Push subscriptions are not permanent — the push service can rotate or invalidate them at any time. The Service Worker listens for pushsubscriptionchange, creates a fresh subscription, and POSTs the new endpoint to subscribeUrl. This works even when no browser tab is open. The VAPID key and server URL needed for this are delivered from the page to the SW via postMessage and persisted in IndexedDB.

Encryption (aes128gcm, RFC 8291)

For each message the server:

  1. Generates an ephemeral P-256 key pair.
  2. Derives an ECDH shared secret with the subscriber's p256dh key.
  3. Applies RFC 8291 §3.4 two-step HKDF to produce the AES-128 content-encryption key and nonce:
    • PRK_key = HMAC-SHA-256(auth_secret, ecdh_secret)
    • IKM = HMAC-SHA-256(PRK_key, "WebPush: info" || ua_public || as_public || 0x01)
    • PRK = HMAC-SHA-256(salt, IKM)
    • CEK = HMAC-SHA-256(PRK, "Content-Encoding: aes128gcm" || 0x01)[0..15]
    • NONCE = HMAC-SHA-256(PRK, "Content-Encoding: nonce" || 0x01)[0..11]
  4. Encrypts the payload with AES-128-GCM.
  5. Prepends the RFC 8188 header (salt || record_size || ephemeral_public_key).

The VAPID JWT (Authorization header) is signed with ES256 and proves the server's identity to the push service.

Browser Support

Browser Supported
Chrome 50+
Firefox 44+
Edge 17+
Safari 16+ (macOS 13+ / iOS 16.4+)
Opera 42+
Internet Explorer

Safari on iOS requires the site to be added to the Home Screen for push to work on iOS 16.4.

Production Checklist

  • VAPID keys generated and stored in .env (never commit them)
  • mds.webpush.sw.js placed at domain root (/public/)
  • subscribeUrl route is protected by authentication middleware
  • Subscribe endpoint is idempotent (same endpoint can be POSTed multiple times safely)
  • Expired subscriptions (404 / 410) are deleted from the database
  • HTTPS is enabled on the production domain
  • Notification icon is at least 192×192 PNG
  • unsubscribeUrl endpoint wired up for clean opt-out (optional but recommended)

License

MIT

Made With ❤️ Love By: md-mojahed.github.io

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 2
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-07-10

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固