调用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
相关产品推荐
相关产品推荐

