.pem 格式(含客戶端憑證 + 私鑰),由 Apple Developer 後台下載 .cer 再轉換# 1. 下載 aps_development.cer (測試) 或 aps_production.cer (正式)
# 2. 雙擊安裝至鑰匙圈
# 3. 鑰匙圈 → 登入 → 憑證 → 右鍵匯出 .p12
# 4. 轉換為 PEM
openssl pkcs12 -in aps_development.p12 -out aps_development.pem -nodes -clcerts
# 輸入匯出密碼,產出 aps_development.pem
<?php
/**
* APNs 推播發送類
*/
class ApnsPush {
private $pemPath;
private $passphrase;
private $gateway;
private $fp;
public function __construct($pemPath, $passphrase = '', $isSandbox = true) {
$this->pemPath = $pemPath;
$this->passphrase = $passphrase;
$this->gateway = $isSandbox
? 'ssl://gateway.sandbox.push.apple.com:2195'
: 'ssl://gateway.push.apple.com:2195';
}
public function connect() {
$ctx = stream_context_create();
stream_context_set_option($ctx, 'ssl', 'local_cert', $this->pemPath);
stream_context_set_option($ctx, 'ssl', 'passphrase', $this->passphrase);
stream_context_set_option($ctx, 'ssl', 'verify_peer', false);
stream_context_set_option($ctx, 'ssl', 'verify_peer_name', false);
$this->fp = stream_socket_client(
$this->gateway, $err, $errstr, 60,
STREAM_CLIENT_CONNECT | STREAM_CLIENT_PERSISTENT, $ctx
);
if (!$this->fp) {
throw new Exception("APNs 連線失敗: $err $errstr");
}
return true;
}
public function push($deviceToken, $alert, $badge = 0, $sound = 'default', $custom = []) {
if (!$this->fp) $this->connect();
// 建構 Payload
$payload = ['aps' => ['alert' => $alert]];
if ($badge) $payload['aps']['badge'] = (int)$badge;
if ($sound) $payload['aps']['sound'] = $sound;
if ($custom) $payload = array_merge($payload, $custom);
$json = json_encode($payload);
if (strlen($json) > 4096) { // APNs 限制 4KB
throw new Exception('Payload 過大 (> 4KB)');
}
// 封裝 Binary 格式 (APNs Protocol v0)
// Command(1) + TokenLength(2) + Token(32) + PayloadLength(2) + Payload
$token = pack('H*', str_replace(' ', '', $deviceToken));
$msg = chr(0) . pack('n', 32) . $token . pack('n', strlen($json)) . $json;
$written = fwrite($this->fp, $msg);
if ($written === false) {
throw new Exception('寫入失敗,連線可能已斷開');
}
return true;
}
public function close() {
if ($this->fp) fclose($this->fp);
}
}
// --- 使用範例 ---
$deviceToken = '6ad7b13fb05e6137a46a60ea421e50164b701671cc176f7033bb9ef438a8aef9';
$pemPath = __DIR__ . '/aps_development.pem';
$passphrase = 'your_passphrase';
try {
$apns = new ApnsPush($pemPath, $passphrase, true); // true = sandbox
$apns->push($deviceToken, '測試推播訊息', 1, 'default', ['id' => 'msg_123']);
echo "推播發送成功\n";
$apns->close();
} catch (Exception $e) {
echo "錯誤: " . $e->getMessage() . "\n";
}
Apple 自 2016 年起支援 HTTP/2 API,優點:
<?php
class ApnsHttp2 {
private $keyId, $teamId, $bundleId, $p8Path;
private $endpoint;
public function __construct($keyId, $teamId, $bundleId, $p8Path, $isSandbox = true) {
$this->keyId = $keyId;
$this->teamId = $teamId;
$this->bundleId = $bundleId;
$this->p8Path = $p8Path;
$this->endpoint = $isSandbox
? 'https://api.sandbox.push.apple.com'
: 'https://api.push.apple.com';
}
private function generateJwt() {
$header = ['alg' => 'ES256', 'kid' => $this->keyId];
$claims = ['iss' => $this->teamId, 'iat' => time()];
$headerB64 = $this->base64urlEncode(json_encode($header));
$claimsB64 = $this->base64urlEncode(json_encode($claims));
$data = "$headerB64.$claimsB64";
$pkey = openssl_pkey_get_private('file://' . $this->p8Path);
openssl_sign($data, $signature, $pkey, 'sha256');
return "$data." . $this->base64urlEncode($signature);
}
private function base64urlEncode($data) {
return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
public function push($deviceToken, $alert, $badge = 0, $sound = 'default', $custom = []) {
$payload = ['aps' => ['alert' => $alert]];
if ($badge) $payload['aps']['badge'] = (int)$badge;
if ($sound) $payload['aps']['sound'] = $sound;
if ($custom) $payload = array_merge($payload, $custom);
$url = "$this->endpoint/3/device/$deviceToken";
$jwt = $this->generateJwt();
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_2_0,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_HTTPHEADER => [
'authorization: bearer ' . $jwt,
'apns-topic: ' . $this->bundleId,
'content-type: application/json',
],
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return [$httpCode, $response];
}
}
| 錯誤 | 原因 | 解決 |
|---|---|---|
| SSL 連線失敗 | 憑證路徑錯誤、Passphrase 錯誤、.pem 格式不正確 | 檢查路徑、用 openssl 驗證 `openssl x509 -in cert.pem -text` |
| Invalid token | Device Token 格式錯誤(含空格、長度不對) | 移除空格,確認 64 字元十六進位 |
| Payload too large | JSON 超過 4096 bytes | 精簡自訂欄位、縮短 alert 文字 |
| Connection reset | APNs 伺服器斷開(錯誤 payload、過期憑證) | 檢查錯誤回應、更新憑證 |
| Expired provider token | JWT 簽發時間過舊 | 確保伺服器時間同步、重新產生 JWT |