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

无法解码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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 20:53:14