简介
本项目提供一组基于 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 类,流程如下:
- 构造 Header:`{“alg”:”ES256″,”kid”:
,”typ”:”JWT”}` - 构造 Payload:
iss: Issuer IDexp: 当前时间 + 1200 秒(官方建议 Token 有效期不超过 20 分钟,可按需调整)aud: 固定为appstoreconnect-v1
- 分别对 Header 与 Payload 做 JSON 编码 + URL-Safe Base64 编码,拼接成
header.payload - 使用私钥对上述字符串做 ECDSA (P-256 + SHA256) 签名:
openssl_sign($msg, $signature, $key, OPENSSL_ALGO_SHA256) - 因 OpenSSL 返回的是 DER 编码,需要转换为原始
R||S64 字节形式,再做 URL-Safe Base64 得到第三段 - 拼接最终
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 的实现共性:
通用模式
error_reporting(0);关闭错误输出(建议生产保留日志而不是完全关闭)。- 用
$_GET读取参数,判空后构造错误 JSON 并exit()。 - 初始化
curl,设置CURLOPT_URL、CURLOPT_HTTPHEADER(主要是 `Authorization: Bearer
`)。 - 执行
curl_exec(),输出响应原文(直接透传 Apple API 返回)。
GetToken (v1/GetToken/index.php)
- 校验
iss和kid是否存在。 - 校验对应私钥文件是否存在。
- 构造 Header/Payload → 调用
ECSign::sign()→ 返回自定义 JSON(包含过期时间与签名后 Token)。
List / Register 系列
- 所有读取型接口:
ListApps、ListDevices、ListBundleIDs、ListCertifications- 仅校验
token。 - 使用 Apple 官方分页与字段过滤参数(示例里固定 limit=200)。
- 仅校验
- 写入型接口:
RegisterNewDevice、RegisterNewBundleID- 额外校验必要属性 (
udid/bid/name)。 - 构造
dataJSON 结构(参考官方规范: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)
- 为什么 Token 有效期只设置 1200 秒?
- Apple 建议短期令牌,减少泄漏风险。可调但不要太长。
- 为什么需要 DER → Raw 签名转换?
- OpenSSL 默认产出 DER 编码,JWT 要求直接拼接原始定长 R+S。
- 出现“cannot find p8 file”如何处理?
- 检查 Key ID 是否正确、文件命名是否遵守 `AuthKey_
.p8`、路径是否和代码一致。
- 检查 Key ID 是否正确、文件命名是否遵守 `AuthKey_
- 是否可以缓存 Token?
- 可以,将 Token 与过期时间存入内存或 Redis,避免频繁重新签名。
- 为什么写操作使用 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;
部署建议
- 使用 Nginx + PHP-FPM,限制
AuthKey目录的外部访问。 - 如果迁移路径,统一通过配置文件引用,不要硬编码绝对路径。
- 加上简单访问统计与告警(如调用失败次数过多)。
许可证
参见仓库内 LICENSE 文件。
进一步阅读
- Apple 官方文档:https://developer.apple.com/documentation/appstoreconnectapi
- JWT 标准:https://datatracker.ietf.org/doc/html/rfc7519
- ECDSA 与 ES256:https://www.rfc-editor.org/rfc/rfc7518
如需添加更多接口(Profiles/TestFlight/Builds 等),可按现有模式快速扩展。欢迎二次开发与提交改进建议。


