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

如何使用PHP CURL生成请求头并实现Web推送通知?

解决PHP CURL发送Web推送通知的请求头及加密问题

你的代码存在两个核心问题:Payload未按规范加密(Web Push要求所有payload必须加密)、VAPID签名请求头未正确生成,以下是完整的修复方案:

1. 辅助工具函数

首先实现Web Push标准要求的URL安全Base64编解码:

// URL安全的Base64编码(替换+/-为-_,去除末尾=)
function base64url_encode($data) {
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

// URL安全的Base64解码(还原+/-和补位=)
function base64url_decode($data) {
    return base64_decode(str_pad(strtr($data, '-_', '+/'), strlen($data) % 4, '=', STR_PAD_RIGHT));
}

2. 生成VAPID授权头

VAPID用于验证推送发起方的身份,需要生成符合ES256算法的JWT签名:

function generateVapidHeaders($vapidKeys, $endpoint) {
    // 提取推送服务的受众(协议+域名)
    $audience = parse_url($endpoint, PHP_URL_SCHEME) . '://' . parse_url($endpoint, PHP_URL_HOST);
    $expiration = time() + 43200; // 签名有效期12小时(最大不超过24小时)

    // JWT头部和载荷
    $header = json_encode(['typ' => 'JWT', 'alg' => 'ES256']);
    $payload = json_encode([
        'aud' => $audience,
        'exp' => $expiration,
        'sub' => $vapidKeys['subject']
    ]);

    // 编码头部和载荷
    $encodedHeader = base64url_encode($header);
    $encodedPayload = base64url_encode($payload);
    $signatureBase = $encodedHeader . '.' . $encodedPayload;

    // 用VAPID私钥生成ES256签名
    $privateKeyPem = '-----BEGIN PRIVATE KEY-----' . PHP_EOL .
                     chunk_split(base64_encode(base64url_decode($vapidKeys['privateKey'])), 64, PHP_EOL) .
                     '-----END PRIVATE KEY-----';
    $privateKey = openssl_pkey_get_details(openssl_pkey_get_private($privateKeyPem));
    openssl_sign($signatureBase, $signature, $privateKey['key'], OPENSSL_ALGO_SHA256);
    $encodedSignature = base64url_encode($signature);

    return [
        'Authorization' => 'WebPush ' . $encodedHeader . '.' . $encodedPayload . '.' . $encodedSignature,
        'Crypto-Key' => 'p256ecdsa=' . $vapidKeys['publicKey']
    ];
}

3. 加密Push Payload

Web Push规范要求payload必须用AES-GCM加密,以下是加密实现:

function encryptPayload($payload, $subscriptionKeys) {
    $plaintext = json_encode($payload);
    $p256dh = base64url_decode($subscriptionKeys['p256dh']);
    $auth = base64url_decode($subscriptionKeys['auth']);

    // 生成随机盐和本地EC密钥对
    $salt = random_bytes(16);
    $localKey = openssl_pkey_new([
        'ec_paramgen_curve' => 'prime256v1',
        'private_key_type' => OPENSSL_KEYTYPE_EC
    ]);
    $localDetails = openssl_pkey_get_details($localKey);
    $localPublicKey = $localDetails['ec']['x'] . $localDetails['ec']['y'];

    // 计算共享密钥
    $remoteKey = openssl_pkey_get_public([
        'key' => $p256dh,
        'type' => OPENSSL_KEYTYPE_EC,
        'curve_name' => 'prime256v1'
    ]);
    openssl_pkey_derive($remoteKey, $localKey, $sharedSecret, 32);

    // 生成派生密钥(符合Web Push规范)
    $info = pack('H*', '77656270757368206b6579'); // "webpush key"
    $prk = hash_hmac('sha256', $auth, $sharedSecret, true);
    $contentEncryptionKey = hash_hmac('sha256', $info . pack('C', 0x01), $prk, true);
    $nonce = hash_hmac('sha256', $info . pack('C', 0x02), $prk, true);
    $nonce = substr($nonce, 0, 12);

    // AES-GCM加密明文
    $tag = '';
    $ciphertext = openssl_encrypt(
        $plaintext,
        'aes-128-gcm',
        substr($contentEncryptionKey, 0, 16),
        OPENSSL_RAW_DATA,
        $nonce,
        $tag
    );

    // 组装加密后的数据:盐 + 本地公钥 + 密文 + 标签
    $encrypted = $salt . $localPublicKey . $ciphertext . $tag;

    return [
        'encryptedPayload' => base64url_encode($encrypted),
        'salt' => base64url_encode($salt),
        'dh' => base64url_encode($localPublicKey)
    ];
}

4. 修复后的推送函数

整合上述逻辑,生成正确的请求头并发送加密后的payload:

public function sendPushNotification($subscription, $payload, $auth) {
    $vapidKeys = $auth['VAPID'];
    // 生成VAPID授权头
    $vapidHeaders = generateVapidHeaders($vapidKeys, $subscription['endpoint']);
    // 加密Payload
    $encryptedData = encryptPayload($payload, $subscription['keys']);

    // 组装完整请求头
    $headers = [
        'Content-Type: application/octet-stream',
        'Authorization: ' . $vapidHeaders['Authorization'],
        'Crypto-Key: ' . $vapidHeaders['Crypto-Key'] . '; dh=' . $encryptedData['dh'],
        'Content-Encoding: aesgcm',
        'Encryption: salt=' . $encryptedData['salt']
    ];

    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $subscription['endpoint']);
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_POST, true);
    curl_setopt($curl, CURLOPT_POSTFIELDS, base64url_decode($encryptedData['encryptedPayload']));
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
    // 部分推送服务(如FCM)要求HTTP/2,需确保curl支持
    curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);

    $response = curl_exec($curl);

    if ($response === false) {
        $error = curl_error($curl);
        echo 'cURL error: ' . $error;
    } else {
        $httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
        if ($httpCode === 201) {
            echo 'Web push notification sent successfully!';
        } else {
            echo 'Push failed with status code: ' . $httpCode . ', response: ' . $response;
        }
    }

    curl_close($curl);
    return $response;
}

关键注意事项

  • HTTP/2支持:FCM等主流推送服务要求使用HTTP/2,可通过curl_version()检查环境是否支持。
  • 状态码判断:推送成功的标准响应码是201 Created,其他状态码需根据响应内容排查问题。
  • 依赖扩展:确保PHP开启openssl扩展,否则无法完成加密和签名操作。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 06:45:03