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

调用Apple Developer API遇401未授权及订阅查询API选型问题

订阅查询API选择及Apple JWT 401错误解决方案

一、背景与问题

我使用Plugin.InAppBilling包实现了订阅购买功能,购买成功后已获取App Store和Play Store的PurchaseToken与ID,现在需要查询用户的订阅详情,同时调用Apple相关API时遇到401 Unauthorized响应,寻求解决方案。

购买功能实现代码:

private async void PlanClicked(Object sender, EventArgs e)
{
    UserDialogs.Instance.ShowLoading("");
    if (IsBusy)
        return;

    IsBusy = true;
    try
    {
        // 检查网络连接
        if (Connectivity.NetworkAccess != NetworkAccess.Internet)
            return;

        // 连接应用商店API
        var connected = await CrossInAppBilling.Current.ConnectAsync();
        if (!connected)
            return;

        UserDialogs.Instance.HideHud();
        // 发起购买请求
        var purchase = await CrossInAppBilling.Current.PurchaseAsync(productId, ItemType.Subscription);

        if (purchase == null)
        {
            // 未完成购买
            return;
        }

        if (purchase.State == PurchaseState.Purchased)
        {
            Debug.WriteLine("Purchase successfull");
            Debug.WriteLine("Purchase token:>>" + purchase.PurchaseToken);
            Debug.WriteLine("Purchase id:>>" + purchase.Id);
        }
        else
        {
            throw new InAppBillingPurchaseException(PurchaseError.GeneralError);
        }
    }
    catch (InAppBillingPurchaseException purchaseEx)
    {
        Debug.WriteLine("purchaseEx:>>" + purchaseEx);
    }
    catch (Exception ex)
    {
        Debug.WriteLine("exception:>>" + ex);
    }
    finally
    {
        await CrossInAppBilling.Current.DisconnectAsync();
        IsBusy = false;
    }
}

二、API选择疑问

我尝试使用Apple的Get All Subscription Statuses API和Google的Purchases.Subscriptionsv2.Get API,这两个API是否适合用于查询订阅详情?

三、Apple API 401错误问题

调用Apple API时收到401 Unauthorized响应,当前生成JWT令牌的代码如下:

private string LoadPrivateKey()
{
    string fileName = "API key (.p8 file)";

    using var stream = FileSystem.OpenAppPackageFileAsync(fileName).Result;
    using var reader = new StreamReader(stream);
    var privateKey = reader.ReadToEnd();

    // 移除头尾标识和换行符
    privateKey = privateKey
        .Replace("-----BEGIN PRIVATE KEY-----", string.Empty)
        .Replace("-----END PRIVATE KEY-----", string.Empty)
        .Replace("\n", string.Empty)
        .Replace("\r", string.Empty);

    return privateKey;
}

public async void JWTGenerator(Object sender, EventArgs e)
{
    try
    {
        // Apple开发者账号中的Key ID
        string keyId = "keyId";

        // Apple开发者账号中的Team ID
        string teamId = "teamId";

        // 加载.p8私钥文件
        string privateKey = LoadPrivateKey();

        // 生成JWT令牌
        string jwtToken = AppleApiTokenGenerator.GenerateToken(keyId, teamId, privateKey);
        DisplayAlert("JWT Token", jwtToken, "OK");
    }
    catch (Exception ex)
    {
        Debug.WriteLine("JWTException:>" + ex);
    }

}

public class AppleApiTokenGenerator
{
    public static string GenerateToken(string keyId, string teamId, string privateKey)
    {
        byte[] privateKeyBytes = Convert.FromBase64String(privateKey);
        var tokenHandler = new JwtSecurityTokenHandler();
        var key = new SymmetricSecurityKey(Convert.FromBase64String(privateKey));
        var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(JwtRegisteredClaimNames.Iss, teamId),
                new Claim(JwtRegisteredClaimNames.Aud, "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/subscriptions/{transactionId}"),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            }),
            Expires = DateTime.UtcNow.AddMinutes(20),
            SigningCredentials = credentials
        };

        var token = tokenHandler.CreateToken(tokenDescriptor);
        Console.WriteLine($"JWT token:{tokenHandler.WriteToken(token)}");
        return tokenHandler.WriteToken(token);
    }
}

已将.p8文件添加到Recourses/Row目录,并设置生成操作为MauAsset。


四、解决方案与指导

1. API选择确认

  • Apple端:Get All Subscription Statuses API完全符合需求,它能返回指定交易ID对应的全量订阅状态信息,包括有效期、自动续期状态、历史交易记录等核心数据。
  • Google端:Purchases.Subscriptionsv2.Get API是官方推荐的v2版本接口,相比旧版v1接口,能提供更完整的订阅生命周期数据,适合当前查询需求。

2. Apple JWT 401错误修复

你的JWT生成代码存在3个核心问题,导致签名验证失败:

  • 签名算法错误:Apple API要求使用**ES256(ECDSA with SHA-256)**非对称加密算法,你当前使用的HmacSha256是对称加密算法,不符合要求。
  • Audience(aud)字段错误:aud需固定为appstoreconnect-v1,而非具体的API请求URL。
  • 缺少Key ID(kid)声明:需在JWT头部添加kid字段,对应Apple开发者账号中创建的API Key的ID。
  • 私钥解析错误:.p8文件是EC格式私钥,不能用SymmetricSecurityKey加载,需使用ECDSA相关类处理。

修复后的JWT生成代码示例:

using System.Security.Cryptography;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Tokens;

public class AppleApiTokenGenerator
{
    public static string GenerateToken(string keyId, string teamId, string privateKeyXml)
    {
        // 加载EC私钥
        using var ecDsa = ECDsa.Create();
        ecDsa.FromXmlString(privateKeyXml);
        var securityKey = new ECDsaSecurityKey(ecDsa);

        var tokenHandler = new JwtSecurityTokenHandler();
        var tokenDescriptor = new SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(new[]
            {
                new Claim(JwtRegisteredClaimNames.Iss, teamId),
                new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
            }),
            Expires = DateTime.UtcNow.AddMinutes(20),
            SigningCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.EcdsaSha256),
            Audience = "appstoreconnect-v1" // 固定值
        };

        // 向JWT头部添加kid字段
        var token = tokenHandler.CreateJwtSecurityToken(tokenDescriptor);
        token.Header.Add("kid", keyId);

        return tokenHandler.WriteToken(token);
    }
}

私钥加载部分需转换为XML格式(ECDSA要求的格式):

private string LoadPrivateKey()
{
    string fileName = "your-key-file.p8";
    using var stream = FileSystem.OpenAppPackageFileAsync(fileName).Result;
    using var reader = new StreamReader(stream);
    var privateKeyContent = reader.ReadToEnd();

    // 清理私钥内容
    var cleanKey = privateKeyContent
        .Replace("-----BEGIN PRIVATE KEY-----", "")
        .Replace("-----END PRIVATE KEY-----", "")
        .Replace("\n", "")
        .Replace("\r", "");

    byte[] keyBytes = Convert.FromBase64String(cleanKey);
    return ConvertPkcs8ToXml(keyBytes);
}

// 将PKCS8格式的.p8私钥转换为XML格式
private string ConvertPkcs8ToXml(byte[] pkcs8Key)
{
    using var ecDsa = ECDsa.Create();
    ecDsa.ImportPkcs8PrivateKey(pkcs8Key, out _);
    return ecDsa.ToXmlString(includePrivateParameters: true);
}

3. 额外注意事项

  • 环境区分:沙盒环境API域名是https://api.storekit-sandbox.itunes.apple.com,生产环境是https://api.storekit.itunes.apple.com,需根据当前环境切换。
  • 权限配置:确保Apple API Key在开发者后台创建时,勾选了In-App Purchase相关权限。
  • 异步优化:JWTGenerator方法使用async void存在线程风险,建议改为async Task。

内容的提问来源于stack exchange,提问作者Matthew Pans

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 04:10:53