如何使用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
相关产品推荐
相关产品推荐

