无法解码Apple内购Server-to-Server通知Payload的问题排查
Apple IAP Server-to-Server通知signedPayload解码失败问题
问题背景
在Laravel中实现Apple应用内购买服务器到服务器通知功能,已配置好issuer_id、key_id及p8私钥,但无法解码App Store传来的signedPayload。
实现代码
class ServerNotificationAppleController extends Controller { private $storeKitKeysUrl = 'https://appleid.apple.com/auth/keys'; public function handleNotification(Request $request) { Log::info('Apple Notification Request:', $request->all()); $signedPayload = $request->input('signedPayload'); if (!$signedPayload) { return response()->json(['error' => 'signedPayload not provided'], 400); } $jwtToken = $this->generateAppleJWT(); $response = Http::withHeaders([ 'Authorization' => 'Bearer ' . $jwtToken, ])->get($this->storeKitKeysUrl); Log::info('Apple Keys Status:', ['status' => $response->status()]); Log::info('Apple Keys Body:', ['body' => $response->body()]); if ($response->status() !== 200) { return response()->json(['error' => "Apple public keys couldn't be retrieved"], 401); } $keysData = $response->json(); $validatedPayload = $this->validateSignedPayload($signedPayload, $keysData); if (!$validatedPayload) { return response()->json(['error' => 'Invalid signedPayload'], 400); } Log::info("Apple Purchase Data:", (array)$validatedPayload); return response()->json(['message' => 'Notification processed successfully'], 200); } private function generateAppleJWT() { $keyId = config('services.apple.key_id'); $issuerId = config('services.apple.issuer_id'); $privateKey = file_get_contents(storage_path(config('services.apple.private_key'))); $nowUtc = Carbon::now(); $expirationUtc = $nowUtc->copy()->addMinutes(20); $payload = [ 'iss' => $issuerId, 'iat' => $nowUtc->timestamp, 'exp' => $expirationUtc->timestamp, 'aud' => 'appstoreconnect-v1', ]; $header = [ 'kid' => $keyId, 'alg' => 'ES256', 'typ' => 'JWT' ]; return JWT::encode($payload, $privateKey, 'ES256', $keyId, $header); } private function validateSignedPayload($signedPayload, $keysData) { try { $jwkKeys = JWK::parseKeySet($keysData); $allowedAlgs = new \stdClass(); $allowedAlgs->algos = ['ES256']; // Using ES256 return JWT::decode($signedPayload, $jwkKeys, $allowedAlgs); } catch (\Exception $e) { Log::error("Apple Purchase Validation Error: " . $e->getMessage() . " Trace: " . $e->getTraceAsString()); return null; } } }
问题现象
已正常接收signedPayload,成功获取Apple公钥(状态码200),但validateSignedPayload()方法解码失败并记录错误。
疑问
- 获取并使用Apple公钥进行验证的方式是否正确?
- 是否需要先提取
signedPayload的特定部分再进行解码? - 问题是否与解析JWK密钥的方式有关?
问题分析与修正方案
1. 公钥获取方式错误
你当前通过带JWT授权的请求获取https://appleid.apple.com/auth/keys的密钥是错误的。Apple IAP服务器通知的signedPayload签名使用的是App Store专用公钥,无需授权即可获取,正确的公钥地址是https://pki.apple.com/verify/appstore,而非Apple ID认证的密钥地址。
2. 无需提取signedPayload特定部分
signedPayload本身就是完整的JWT令牌,直接传入解码方法即可,不需要拆分。
3. JWK解析与解码参数问题
使用firebase/php-jwt库时,decode方法的第三个参数应为数组格式的允许算法,而非stdClass对象。
修正后的代码
修正公钥获取逻辑
// 替换原storeKitKeysUrl private $storeKitKeysUrl = 'https://pki.apple.com/verify/appstore'; // 移除JWT授权,直接发起GET请求 $response = Http::get($this->storeKitKeysUrl);
修正validateSignedPayload方法
private function validateSignedPayload($signedPayload, $keysData) { try { $jwkKeys = JWK::parseKeySet($keysData); // 第三个参数改为数组格式的允许算法 return JWT::decode($signedPayload, $jwkKeys, ['ES256']); } catch (\Exception $e) { Log::error("Apple Purchase Validation Error: " . $e->getMessage() . " Trace: " . $e->getTraceAsString()); return null; } }
额外注意事项
- 确保p8私钥文件权限正确(推荐600权限),且文件路径配置无误;
- 验证JWT时建议添加额外校验:比如
iss字段应为https://appleid.apple.com,aud字段需匹配你的App Bundle ID; - 缓存Apple公钥,避免每次请求都去获取,提升性能。
内容的提问来源于stack exchange,提问作者muratapps
相关产品推荐
相关产品推荐

