PHP 实现 App Store Connect API

72次阅读
没有评论

简介

本项目提供一组基于 App Store Connect API 的轻量级 PHP 示例接口,覆盖获取 JWT Token、注册设备、列出设备、注册 BundleID、列出 BundleID、列出 App、列出证书等常用操作,并包含自实现的 ES256 签名逻辑(无需第三方库)。

GitHub 仓库:https://github.com/ty-yqs/App-Store-Connect-API


目录结构

AuthKey/               # 存放 Apple 私钥 .p8 文件 (命名规范: AuthKey_KeyID.p8)
index.html             # (若需要,可以放简单说明或重定向)
README.md              # 英文说明
README.zh-Hans.md      # 中文说明
v1/
  GetToken/
    index.php          # 生成 JWT Token
    jwt.php            # ECSign 类, 实现 ES256 JWT 签名
  ListApps/index.php   # 获取 App 列表
  ListBundleIDs/index.php    # 获取 BundleID 列表
  ListCertifications/index.php # 获取证书列表
  ListDevices/index.php       # 获取设备列表
  RegisterNewBundleID/index.php # 注册新的 BundleID
  RegisterNewDevice/index.php  # 注册新设备 

快速开始

1. 准备 Apple API 账号信息

登录 App Store Connect → 用户与访问 →“密钥”页,获得:

  • Issuer ID (iss)
  • Key ID (kid)
  • 下载与之对应的 .p8 私钥文件

2. 上传私钥文件

将私钥放入 AuthKey 目录,命名格式必须为 AuthKey_KeyID.p8
示例:AuthKey_AB12CD34.p8

代码中目前使用的是绝对路径:/www/wwwroot/***/AuthKey/AuthKey_kid.p8。如果你部署在其它目录,请修改 v1/GetToken/index.php 中的 file_exists()file_get_contents() 路径以适配。

3. 获取 Token

访问:

GET /v1/GetToken?iss=
<IssuerID>&kid=<KeyID>

成功返回:

{
  "status": "200",
  "expiration": 1733040000,
  "token": "
<JWT_TOKEN>"
}

token 即 Bearer Token,后续其它接口需携带:

Authorization: Bearer 
<JWT_TOKEN>

4. 调用其它接口

示例列设备:

GET /v1/ListDevices?token=
<JWT_TOKEN>

curl 示例:

curl "https://<your-host>/v1/GetToken?iss=<ISSUER_ID>&kid=<KEY_ID>"
# 然后:curl "https://<your-host>/v1/ListDevices?token=<TOKEN>"

JWT 签名流程详解 (ES256)

核心实现位于 v1/GetToken/jwt.php 中的 ECSign 类,流程如下:

  1. 构造 Header:`{“alg”:”ES256″,”kid”:
    ,”typ”:”JWT”}`
  2. 构造 Payload:
    • iss: Issuer ID
    • exp: 当前时间 + 1200 秒(官方建议 Token 有效期不超过 20 分钟,可按需调整)
    • aud: 固定为 appstoreconnect-v1
  3. 分别对 Header 与 Payload 做 JSON 编码 + URL-Safe Base64 编码,拼接成 header.payload
  4. 使用私钥对上述字符串做 ECDSA (P-256 + SHA256) 签名:openssl_sign($msg, $signature, $key, OPENSSL_ALGO_SHA256)
  5. 因 OpenSSL 返回的是 DER 编码,需要转换为原始 R||S 64 字节形式,再做 URL-Safe Base64 得到第三段
  6. 拼接最终 header.payload.signature

DER 与 Raw Signature 转换

fromDER($der, 64):将 DER 编码的签名拆解为两个 32 字节整数(R、S),并左侧补零对齐。这样符合 JWT 对 ES256 的期望格式。

关键方法说明

  • sign($payload, $header, $key): 主入口,负责整体打包与编码。
  • _sign($msg, $key): 调用 openssl_sign,并转换签名格式。
  • urlsafeB64Encode() / urlsafeB64Decode(): 实现 JWT 所需的 URL 安全 Base64(去掉 = 补位)。
  • jsonEncode() / jsonDecode(): 包装 JSON 处理并抛出更明确的异常。
  • toDER()/fromDER(): 与整数正负处理相关方法一起,确保签名格式正确。

可配置项建议

  • exp 有效期可通过定义常量或配置文件统一管理。
  • 私钥路径应抽象为配置(如 config.php),避免硬编码。

各接口代码逻辑解读

下面以典型结构总结每个 index.php 的实现共性:

通用模式

  1. error_reporting(0); 关闭错误输出(建议生产保留日志而不是完全关闭)。
  2. $_GET 读取参数,判空后构造错误 JSON 并 exit()
  3. 初始化 curl,设置 CURLOPT_URLCURLOPT_HTTPHEADER(主要是 `Authorization: Bearer
    `)。
  4. 执行 curl_exec(),输出响应原文(直接透传 Apple API 返回)。

GetToken (v1/GetToken/index.php)

  • 校验 isskid 是否存在。
  • 校验对应私钥文件是否存在。
  • 构造 Header/Payload → 调用 ECSign::sign() → 返回自定义 JSON(包含过期时间与签名后 Token)。

List / Register 系列

  • 所有读取型接口:ListAppsListDevicesListBundleIDsListCertifications
    • 仅校验 token
    • 使用 Apple 官方分页与字段过滤参数(示例里固定 limit=200)。
  • 写入型接口:RegisterNewDeviceRegisterNewBundleID
    • 额外校验必要属性 (udid / bid / name)。
    • 构造 data JSON 结构(参考官方规范:type + attributes)。
    • CURLOPT_CUSTOMREQUEST='POST' 并附带 CURLOPT_POSTFIELDS

错误处理现状

  • 当前仅做参数缺失的简单 409 响应。
  • 建议:
    • 捕获 curl_error() 输出网络错误。
    • 对 Apple 返回的错误 JSON 可再包装,提升前端一致性。

接口一览 (速查)

接口 方法 说明
/v1/GetToken GET 生成 JWT 用于后续调用
/v1/ListDevices GET 列出已注册设备 (只返回 udid 字段)
/v1/RegisterNewDevice GET (建议改 POST) 注册新设备
/v1/ListBundleIDs GET 列出 BundleID
/v1/RegisterNewBundleID GET (建议改 POST) 注册 BundleID
/v1/ListApps GET 列出应用基本信息
/v1/ListCertifications GET 列出证书

注意:目前“写操作”接口使用 GET + query 参数来提交数据,不符合 REST 语义。生产建议改为真正的 POST 并通过请求体传参。


改进与扩展建议

  • 目录规范:将所有接口调用封装为一个 lib/AppleClient.php,统一复用 cURL 逻辑。
  • 错误码:区分客户端参数错误 (400) 与资源冲突 (409),不要都用 409。
  • 安全:
    • 不要在仓库中提交真实 .p8 文件。
    • Token 生成接口最好加速率限制 (如 IP 限流)。
    • 返回中不建议直接暴露内部错误细节。
  • 配置:将 Issuer ID、Key ID、AuthKey 路径、Token 有效期等集中在 config.php
  • 依赖:可引入 composer 管理,并使用成熟的 JWT 库(如 firebase/php-jwt + ECDSA 支持)减少维护成本。
  • 日志:添加访问日志与错误日志(例如用 error_log() 或 Monolog)。

常见问题 (FAQ)

  1. 为什么 Token 有效期只设置 1200 秒?
    • Apple 建议短期令牌,减少泄漏风险。可调但不要太长。
  2. 为什么需要 DER → Raw 签名转换?
    • OpenSSL 默认产出 DER 编码,JWT 要求直接拼接原始定长 R+S。
  3. 出现“cannot find p8 file”如何处理?
    • 检查 Key ID 是否正确、文件命名是否遵守 `AuthKey_
      .p8`、路径是否和代码一致。
  4. 是否可以缓存 Token?
    • 可以,将 Token 与过期时间存入内存或 Redis,避免频繁重新签名。
  5. 为什么写操作使用 GET?
    • 仅为演示方便,生产需改为 POST,并避免参数明文出现在 URL 中。

代码片段速览

生成签名核心

$header = ['alg' => 'ES256', 'kid' => $kid, 'typ' => 'JWT'];
$payload = [
  'iss' => $iss,
  'exp' => time() + 1200,
  'aud' => 'appstoreconnect-v1'
];
$token = ECSign::sign($payload, $header, $key);

通用 cURL 调用模板

$curl = curl_init();
curl_setopt_array($curl, [
  CURLOPT_URL => $url,
  CURLOPT_RETURNTRANSFER => true,
  CURLOPT_CUSTOMREQUEST => 'GET', // 或 POST
  CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$_GET['token'] ]
]);
$response = curl_exec($curl);
curl_close($curl);
echo $response;

部署建议

  1. 使用 Nginx + PHP-FPM,限制 AuthKey 目录的外部访问。
  2. 如果迁移路径,统一通过配置文件引用,不要硬编码绝对路径。
  3. 加上简单访问统计与告警(如调用失败次数过多)。

许可证

参见仓库内 LICENSE 文件。


进一步阅读

如需添加更多接口(Profiles/TestFlight/Builds 等),可按现有模式快速扩展。欢迎二次开发与提交改进建议。

正文完
 0
评论(没有评论)

YanQS's Blog