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

