You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在PHP开发的PWA应用中用CURL实现推送通知?

PWA Web Push 纯PHP+CURL实现方案

一、生成VAPID Authorization头

VAPID授权依赖ES256算法的JWT签名,需用你的公私钥构造请求头,步骤如下:

1. 将VAPID密钥转为PEM格式

PHP的OpenSSL函数需要PEM格式密钥,先把你的base64url格式密钥转换:

function vapidKeysToPem($base64Key) {
    // 替换base64url特殊字符
    $base64 = str_replace(['-', '_'], ['+', '/'], $base64Key);
    // 补全base64 padding
    $base64 = str_pad($base64, strlen($base64) + (4 - strlen($base64) % 4) % 4, '=');
    $binary = base64_decode($base64);
    // 构造PEM结构
    $pem = "-----BEGIN PRIVATE KEY-----\n" . chunk_split(base64_encode($binary), 64, "\n") . "-----END PRIVATE KEY-----";
    return $pem;
}

2. 生成VAPID JWT

JWT包含受众(推送服务域名)、过期时间、主题(联系方式或网站地址):

function generateVapidJwt($vapidPrivateKeyPem, $audience, $subject) {
    $header = json_encode(['typ' => 'JWT', 'alg' => 'ES256']);
    $payload = json_encode([
        'aud' => $audience, // 比如"https://fcm.googleapis.com"
        'exp' => time() + 43200, // 12小时有效期
        'sub' => $subject // 格式:"mailto:your@email.com" 或 "https://your-site.com"
    ]);

    // 转base64url格式
    $encodedHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
    $encodedPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload));
    $signatureInput = $encodedHeader . '.' . $encodedPayload;

    // ES256签名
    openssl_sign($signatureInput, $signature, $vapidPrivateKeyPem, OPENSSL_ALGO_SHA256);
    $encodedSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));

    return $encodedHeader . '.' . $encodedPayload . '.' . $encodedSignature;
}

3. 构造Authorization头

$vapidPublicKey = '123456789...'; // 你的VAPID公钥(base64url)
$vapidPrivateKey = 'abcdefg....'; // 你的VAPID私钥(base64url)
$subscription = json_decode('{"endpoint":"https://fcm.googleapis.com/fcm/send/eCCP-Bu3sXQ:APA91bFbbjw....","expirationTime":null,"keys":{"p256dh":"BIwBDcsp.....","auth":"RKGkA...."}}', true);

// 提取推送服务域名作为audience
$parsedEndpoint = parse_url($subscription['endpoint']);
$audience = $parsedEndpoint['scheme'] . '://' . $parsedEndpoint['host'];

$vapidPrivateKeyPem = vapidKeysToPem($vapidPrivateKey);
$jwt = generateVapidJwt($vapidPrivateKeyPem, $audience, 'mailto:your@email.com');

$authorizationHeader = "Authorization: vapid t=$jwt, k=$vapidPublicKey";

二、加密推送消息

Web Push要求消息必须用AES-GCM加密,需结合用户订阅的p256dh和auth密钥处理:

function encryptWebPushMessage($message, $userPublicKeyBase64, $userAuthKeyBase64) {
    // 解码用户的公钥和auth密钥
    $userPublicKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $userPublicKeyBase64));
    $userAuthKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $userAuthKeyBase64));

    // 生成临时EC密钥对(P-256曲线)
    $tempKeyPair = openssl_pkey_new([
        'curve_name' => 'prime256v1',
        'private_key_type' => OPENSSL_KEYTYPE_EC,
    ]);
    openssl_pkey_export($tempKeyPair, $tempPrivateKeyPem);
    $tempPublicKeyDetails = openssl_pkey_get_details($tempKeyPair);
    $tempPublicKey = $tempPublicKeyDetails['ec']['pub_key'];

    // ECDH计算共享密钥
    $sharedSecret = openssl_pkey_derive($tempPublicKey, $tempPrivateKeyPem, 32);

    // HKDF派生加密密钥和nonce
    $salt = $userAuthKey;
    $infoEnc = "Content-Encoding: aesgcm128\0";
    $infoNonce = "Content-Encoding: nonce\0";

    $encryptionKey = hash_hkdf('sha256', $sharedSecret, 16, $infoEnc, $salt, true);
    $nonce = hash_hkdf('sha256', $sharedSecret, 12, $infoNonce, $salt, true);

    // 加密消息(末尾加null终止符)
    $plaintext = $message . "\0";
    $ciphertext = openssl_encrypt($plaintext, 'aes-128-gcm', $encryptionKey, OPENSSL_RAW_DATA, $nonce, $tag);
    
    // 拼接临时公钥 + 密文 + 标签
    $encryptedPayload = $tempPublicKey . $ciphertext . $tag;

    // 转base64url格式
    return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($encryptedPayload));
}

三、完整CURL推送代码

整合上述逻辑,替换你原有的代码:

// 配置参数
$vapidPublicKey = '123456789...';
$vapidPrivateKey = 'abcdefg....';
$subscriptionJson = '{"endpoint":"https://fcm.googleapis.com/fcm/send/eCCP-Bu3sXQ:APA91bFbbjw....","expirationTime":null,"keys":{"p256dh":"BIwBDcsp.....","auth":"RKGkA...."}}';
$messageTitle = 'My title';
$messageBody = 'Your message content here';
$clickAction = 'https://www.website.com/';
$iconUrl = 'https://www.website.com/icon.png';

// 解析订阅信息
$subscription = json_decode($subscriptionJson, true);
$endpoint = $subscription['endpoint'];
$userP256dh = $subscription['keys']['p256dh'];
$userAuth = $subscription['keys']['auth'];

// 生成VAPID授权头
function vapidKeysToPem($base64Key) {
    $base64 = str_replace(['-', '_'], ['+', '/'], $base64Key);
    $base64 = str_pad($base64, strlen($base64) + (4 - strlen($base64) % 4) % 4, '=');
    $binary = base64_decode($base64);
    $pem = "-----BEGIN PRIVATE KEY-----\n" . chunk_split(base64_encode($binary), 64, "\n") . "-----END PRIVATE KEY-----";
    return $pem;
}

function generateVapidJwt($vapidPrivateKeyPem, $audience, $subject) {
    $header = json_encode(['typ' => 'JWT', 'alg' => 'ES256']);
    $payload = json_encode([
        'aud' => $audience,
        'exp' => time() + 43200,
        'sub' => $subject
    ]);

    $encodedHeader = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($header));
    $encodedPayload = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($payload));
    $signatureInput = $encodedHeader . '.' . $encodedPayload;

    openssl_sign($signatureInput, $signature, $vapidPrivateKeyPem, OPENSSL_ALGO_SHA256);
    $encodedSignature = str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($signature));

    return $encodedHeader . '.' . $encodedPayload . '.' . $encodedSignature;
}

$parsedEndpoint = parse_url($endpoint);
$audience = $parsedEndpoint['scheme'] . '://' . $parsedEndpoint['host'];
$vapidPrivateKeyPem = vapidKeysToPem($vapidPrivateKey);
$jwt = generateVapidJwt($vapidPrivateKeyPem, $audience, 'mailto:your@email.com');
$authorizationHeader = "Authorization: vapid t=$jwt, k=$vapidPublicKey";

// 构造通知Payload并加密
$notificationPayload = json_encode([
    'title' => $messageTitle,
    'body' => $messageBody,
    'click_action' => $clickAction,
    'icon' => $iconUrl
]);

function encryptWebPushMessage($message, $userPublicKeyBase64, $userAuthKeyBase64) {
    $userPublicKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $userPublicKeyBase64));
    $userAuthKey = base64_decode(str_replace(['-', '_'], ['+', '/'], $userAuthKeyBase64));

    $tempKeyPair = openssl_pkey_new([
        'curve_name' => 'prime256v1',
        'private_key_type' => OPENSSL_KEYTYPE_EC,
    ]);
    openssl_pkey_export($tempKeyPair, $tempPrivateKeyPem);
    $tempPublicKeyDetails = openssl_pkey_get_details($tempKeyPair);
    $tempPublicKey = $tempPublicKeyDetails['ec']['pub_key'];

    $sharedSecret = openssl_pkey_derive($tempPublicKey, $tempPrivateKeyPem, 32);

    $salt = $userAuthKey;
    $infoEnc = "Content-Encoding: aesgcm128\0";
    $infoNonce = "Content-Encoding: nonce\0";

    $encryptionKey = hash_hkdf('sha256', $sharedSecret, 16, $infoEnc, $salt, true);
    $nonce = hash_hkdf('sha256', $sharedSecret, 12, $infoNonce, $salt, true);

    $plaintext = $message . "\0";
    $ciphertext = openssl_encrypt($plaintext, 'aes-128-gcm', $encryptionKey, OPENSSL_RAW_DATA, $nonce, $tag);
    $encryptedPayload = $tempPublicKey . $ciphertext . $tag;

    return str_replace(['+', '/', '='], ['-', '_', ''], base64_encode($encryptedPayload));
}

$encryptedPayload = encryptWebPushMessage($notificationPayload, $userP256dh, $userAuth);

// 发起CURL请求
$headers = [
    $authorizationHeader,
    'Content-Type: application/octet-stream',
    'Content-Encoding: aesgcm',
    'TTL: 60'
];

$ch = curl_init($endpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // 生产环境必须开启SSL验证
curl_setopt($ch, CURLOPT_POSTFIELDS, base64_decode(str_replace(['-', '_'], ['+', '/'], $encryptedPayload)));

$output = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// 结果处理
if ($httpCode == 201) {
    echo "推送成功";
} else {
    echo "推送失败,HTTP状态码: $httpCode,响应: $output";
}

关键注意事项

  • PHP版本要求:必须PHP 7.1+,依赖openssl_pkey_derive和hash_hkdf函数。
  • SSL验证:生产环境禁止关闭CURLOPT_SSL_VERIFYHOST和CURLOPT_SSL_VERIFYPEER,避免安全风险。
  • VAPID密钥生成:可通过OpenSSL命令生成合规密钥:openssl ecparam -name prime256v1 -genkey -noout -out vapid_private.pem,再导出公钥。
  • 前端处理:Service Worker需监听push事件,解密并解析Payload显示通知。

内容的提问来源于stack exchange,提问作者MrBrown

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 04:33:11