App Store Connect API 401:2026 JWT 怎么修?

本地脚本可以生成 JWT,但远程 Runner 返回 401:先别撤销密钥,先确认接口、密钥来源和运行环境是否属于同一条认证链。

最快解法:依次核对 App Store Connect API 端点、API Key 类型、JWT 的 algkidissaud/时间字段、角色权限,以及远程 Mac 的凭据注入;只有最小请求仍持续失败时,才携带请求 ID 向 Apple 反馈。

这篇文章适合三类人:通过 fastlane 上传 TestFlight、但 API Key 认证突然失败的独立开发者;把发布任务迁移到远程 Mac 后发现本地成功、远程返回 401 的维护者;以及自行生成 JWT、直接调用 App Store Connect API 的后端或自动化开发者。

先确认 401 到底来自哪条接口

“JWT 能生成”不等于“这把密钥适用于当前任务”。一个脱敏后的典型故障是:本地脚本能输出三段式 JWT,签名校验也通过;但同一项目迁移到远程 Mac 后,调用 https://api.appstoreconnect.apple.com/v1/apps 返回 401,随后 fastlane 的 TestFlight 上传也失败。

这时不要立即创建第二把密钥。先记录以下信息:

  • 请求地址和 HTTP 方法;
  • HTTP 状态码与响应中的 Apple 错误码;
  • 响应头或日志中的请求 ID;
  • 触发请求的工具,例如自写脚本、pilotupload_to_testflight 或 Transporter;
  • 执行入口,是本地终端、SSH 会话还是 CI Runner;
  • 使用的是 App Store Connect API、App Store Server API,还是单纯的 Transporter 上传。

Apple 的 App Store Connect API 错误响应说明建议结合 HTTP 状态码、错误响应中的 code,以及必要时的 source 来定位问题,而不是只看终端最后一行“Unauthorized”。你可以先把认证失败、权限不足、资源不存在和服务器异常分开记录。

还要把上传链路拆开。App Store Connect REST API 负责元数据、构建记录和部分自动化操作;二进制上传可以使用 Xcode、Transporter 或其他工具。API 请求返回 401,与构建上传后仍处于处理状态,不是同一个故障阶段。

密钥入口不同,.p8 文件也不能互换

最容易被忽略的错误,是把“文件格式相同”误认为“服务用途相同”。

App Store Connect API 的密钥在 App Store Connect 的 Users and Access → Integrations → App Store Connect API 中生成;App Store Server API、Advanced Commerce API 和 External Purchase Server API 使用的是 In-App Purchase 入口生成的密钥。Apple 的 App Store Connect API 密钥说明明确区分了密钥入口、用途和授权范围。

因此,下面这种判断是错误的:

“文件名是 AuthKey_XXXXXX.p8,所以它应该能调用所有 Apple API。”

正确的核对方式是:

  • 登录 App Store Connect,确认密钥所在的 Integrations 子入口;
  • 将密钥页面显示的 Key ID 与 JWT Header 中的 kid 对照;
  • 如果是团队密钥,确认 Issuer ID 来自同一团队;
  • 不要根据 .p8 文件名、下载目录或旧项目配置猜用途;
  • 如果调用的是 App Store Server API,确认端点和密钥来自 In-App Purchase 入口,而不是 App Store Connect API 入口。

团队密钥和个人密钥也不能只靠名称判断。团队密钥面向团队应用范围,个人密钥则受关联用户的角色和应用权限影响。你应当以 Apple 后台实际显示的密钥类型、Key ID 和授权范围为证据,而不是根据文件名推断。

这也解释了一个常见误区:你可能换了一把“看起来更高级”的密钥,却仍然收到 401,因为真正的问题是服务入口、调用端点或 Issuer ID 不匹配,而不是密钥数量不够。

JWT 生成成功,为什么 Apple 仍会拒绝?

JWT 排查应当拆成 Header、Payload 和签名输入三层,不要只运行一条“生成 Token”命令。

Header 层

App Store Connect API 要求使用:

  • algES256
  • kid:App Store Connect API Key 的 Key ID;
  • typJWT

Apple 的 JWT 生成要求明确要求 App Store Connect API 的 JWT 使用 ES256,并且 kid 必须对应签名所用的私钥。Header 里的 Key ID 和实际读取的 .p8 私钥不属于同一对,也会导致认证失败。

Payload 层

团队密钥通常需要检查:

  • iss:对应团队的 Issuer ID;
  • iat:令牌签发时间;
  • exp:令牌过期时间;
  • audappstoreconnect-v1
  • scope:如果使用,必须覆盖实际请求路径。

个人密钥的 Payload 结构不同,Apple 文档要求使用 sub: user,而不是照搬团队密钥的 iss。因此,团队密钥配置复制到个人密钥上,或者反过来套用,可能导致令牌结构看似完整、实际仍被拒绝。

exp 也不能随意设置得很远。对大多数请求,Apple 通常拒绝有效期超过 20 分钟 的 Token;只有满足特定条件的、限定范围的 GET 请求,才可能使用更长生命周期。这里的 20 分钟 是 Apple 官方文档中的令牌规则,不是 fastlane 自己发明的限制。

签名输入层

最后确认三件事:

  • .p8 文件没有被截断;
  • 私钥内容在环境变量中保留了换行,或经过正确的 Base64 编码;
  • 生成 JWT 的进程没有读取到另一份同名文件。

不要把完整 JWT、私钥、Key ID、Issuer ID、Bundle ID 或主机地址贴进工单和公开日志。示例中只使用这样的占位符:

KEY_ID=<REDACTED_KEY_ID>
ISSUER_ID=<REDACTED_ISSUER_ID>
PRIVATE_KEY=<REDACTED_P8_CONTENT>
APP_ID=<REDACTED_APP_ID>

远程 Mac 特别需要检查系统时间。重点不是“网络能否访问 Apple”,而是 Runner 生成 iatexp 时,系统时间是否因休眠恢复、快照回滚、手动改时钟或同步延迟而偏离可信时间源。你可以分别记录本地终端与远程 Mac 的当前时间、时区和 UTC 时间,再重新生成一次短生命周期 Token。

角色和应用范围会怎样阻断请求?

认证通过与有权执行操作是两件事。

App Store Connect API Key 的角色决定了它可以访问哪些 API 能力。Apple 的 角色权限说明列出了 Account Holder、Admin、App Manager、Developer、Marketing 等角色的权限差异;其中 Account Holder 还负责请求 App Store Connect API 访问权限等团队级操作。

排查时建议先做一个权限较小、资源明确的只读请求,再逐步接近真实任务:

  1. 先请求团队中确认存在的应用列表或单个应用资源;
  2. 再验证目标应用是否属于当前团队;
  3. 最后执行 TestFlight 构建上传或发布相关动作。

如果是个人密钥,还要检查用户是否被限制了应用访问范围。用户管理页面中的 App 访问设置,可能让同一账号只能访问部分应用;因此“能登录后台”不代表“能通过 API 访问目标 App”。

不要把以下情况全部归类为 401:

  • Token 失效或签名不正确,属于认证链问题;
  • Token 已被识别,但角色不允许某项操作,属于授权问题;
  • 用户看不到目标 App,属于应用访问范围问题;
  • 团队协议、API 访问申请或账号状态未完成,属于账号状态问题;
  • 请求地址错误,可能实际上调用了另一套 API。

如果最小只读请求已经能够返回资源,而上传动作仍失败,排查重点就应从 JWT 转向 fastlane action、构建文件、代码签名或 Transporter 阶段,不要继续无效轮换 API Key。

fastlane 与远程 Mac 的凭据注入

当本地成功、远程失败时,fastlane 配置通常比 JWT 算法本身更值得优先检查。

app_store_connect_api_key 可能读取 key_idissuer_idkey_filepathkey_content;不同写法对文件路径、换行和环境变量作用域的要求不同。fastlane 官方 App Store Connect API 使用说明还列出了 API Key JSON、key_filepathkey_content 等常见配置方式,并说明部分 action 可以使用 API Key 认证。

建议把同一份脱敏配置放入三个入口分别测试:

  • 本地交互式终端;
  • 通过 SSH 登录的远程 Mac 会话;
  • CI Runner 或无人值守任务。

每次记录当前用户、工作目录、环境变量是否存在、私钥文件权限、文件大小和系统时间。最常见的差异包括:

  • SSH 会话加载了 .zshrc,Runner 没有加载;
  • 变量只配置在交互式 Shell,CI 任务使用的是另一套作用域;
  • key_filepath 使用相对路径,而 Runner 的工作目录不同;
  • key_content 中的换行被压缩成字面量 \n
  • fastlane 读取了旧的 JSON 文件,而不是你刚更新的环境变量;
  • 代码签名证书、Provisioning Profile 和 App Store Connect API Key 被混在同一个凭据目录中。

⚠️ 注意:API Key 私钥、代码签名私钥和上传账号凭据不是同一种东西。即使它们都服务于发布流程,也不要为了“方便”把 .p8 放进源码仓库、构建产物或可下载的日志附件中。

在远程 Mac 上,最好将凭据加载、最小认证请求和真实上传拆成独立步骤。这样当任务失败时,你能判断是环境变量没有注入、JWT 无法生成、API 返回 401,还是构建文件已经上传但后续处理尚未完成。

FAQ:四个最容易误判的场景

App Store Connect API 为什么会返回 401 NOT_AUTHORIZED?

通常先看认证链,而不是先换密钥。重点检查请求是否真的发往 App Store Connect API、kid 是否对应当前私钥、团队密钥是否带有正确的 issaud 是否为 appstoreconnect-v1,以及 iatexp 是否受远程 Mac 系统时间影响。还要保留 Apple 返回的请求 ID,便于后续区分令牌问题和账号状态问题。

JWT 本地验证成功,为什么 Apple 仍然拒绝请求?

本地验证只能证明某个 JWT 库能够解析或验签,不能证明 Apple 会接受它。Apple 还会检查密钥所属服务、Key ID、Issuer ID、Audience、生命周期、请求路径和角色范围。尤其要警惕把 App Store Server API 的密钥用于 App Store Connect API,或者在远程环境中读取了另一份 .p8 文件。

fastlane 使用 API Key 上传 TestFlight,为什么认证失败?

先确认 fastlane 的实际输入,而不是只检查配置文件是否存在。打印脱敏后的 key_id、是否读取到 issuer_id、私钥来源是 key_filepath 还是 key_content,再分别在本地、SSH 和 Runner 中运行最小测试。确认认证链后,再执行一次 pilotupload_to_testflight,否则你无法判断失败来自 JWT 还是后续上传阶段。

两类 Apple API 密钥是否可以互相替代?

不能仅凭 .p8 扩展名互相替代。App Store Connect API Key 用于 App Store Connect API;In-App Purchase 入口的密钥用于另一组 Apple 服务。必须同时核对密钥创建入口、调用域名、JWT 字段和目标接口;如果四项信息无法对应,继续重建 Token 不会解决根本问题。

用最小请求和真实上传完成验收

修复不应以“JWT 文件生成成功”作为结束标准,而应分两阶段验收。

第一阶段是最小认证请求:

  • 使用目标 Runner 的真实运行入口;
  • 使用最终准备部署的那组 Key ID、Issuer ID 和私钥;
  • 请求一个权限允许、资源明确的只读端点;
  • 保存 HTTP 状态码、Apple 错误码、请求 ID 和执行时间;
  • 不保存完整 JWT,不上传私钥,不把敏感 Header 写入普通日志。

第二阶段是真实业务任务:

  • 执行一次 TestFlight 构建上传;
  • 确认 fastlane 使用的 action 与预期一致;
  • 记录上传开始、认证成功、文件传输、Apple 接收和后续处理等阶段;
  • 将构建编号、执行入口和密钥标识的脱敏值关联起来。

上传的构建需要经过系统处理后才会在 App Store Connect 中出现,因此“API 认证成功”与“构建已经可供 TestFlight 使用”之间仍有处理阶段,不能只看上传命令退出码。

你可以使用下面的验收清单:

  • [ ] 已确认调用的是 App Store Connect API,而不是 App Store Server API;
  • [ ] 已从 Apple 的正确入口核对密钥类型;
  • [ ] kid 与实际签名私钥属于同一把密钥;
  • [ ] 团队密钥使用正确的 iss,个人密钥没有误套团队字段;
  • [ ] audiatexpalg 已按官方规则检查;
  • [ ] 本地、SSH、CI Runner 的系统时间已对照;
  • [ ] 已确认密钥角色和目标 App 访问范围;
  • [ ] fastlane 的路径、内容、换行和环境变量作用域一致;
  • [ ] 最小只读请求已成功;
  • [ ] 真实 TestFlight 上传已完成一次;
  • [ ] 日志只保留请求 ID 和脱敏诊断信息。

只有在不同运行入口、正确密钥类型和官方允许的最小请求上都持续失败时,才考虑轮换密钥。若怀疑私钥泄露,撤销是安全事件处置,而不是普通的 401 修复动作;撤销后的 API Key 不能恢复,已经使用该密钥的服务也会受到影响。

在撤销前,先准备替代密钥、更新本地和远程 Mac 的凭据注入、完成最小请求验证,再安排旧密钥撤销和回退窗口。这样即使新密钥配置失败,也不会把本地脚本、远程 Runner 和线上发布任务同时切断。

如果本地环境、远程 Mac 和 CI Runner 使用的是三套不可追踪的配置,当前方案的缺点通常不在 Apple API 本身,而在于凭据路径分散、日志无法复现、无人值守任务没有稳定工作目录,以及出错后很难确认到底执行了哪一把密钥。对于需要持续上传 TestFlight 的独立开发者,保留一台权限清晰、环境固定、日志可查的远程 Mac,往往比反复在临时机器上重建发布环境更容易验收。

你可以先通过 KVMFLUX 的远程 Mac 使用场景了解是否适合把发布任务独立出来;如果只是临时验证 API 认证链,可先按周测试,只有在真实上传、日志留存和 Runner 状态都稳定后,再考虑按月作为常驻发布机。需要进一步确认服务范围时,再查看 KVMFLUX 的常见问题套餐页面

常见问题 FAQ

为什么 JWT 在本地能生成,App Store Connect API 仍然返回 401?

JWT 能被本地库成功签名,只能证明私钥和令牌结构暂时可用,不代表 Apple 接受它。你还需要核对调用端点、密钥所属服务、kid 与私钥是否匹配、iss 或 sub 是否正确、aud 是否为 appstoreconnect-v1、exp 与远程 Mac 时钟是否有效,以及密钥角色是否允许目标请求。

App Store Connect API Key 可以和 In-App Purchase Key 混用吗?

不能。App Store Connect API 使用 Users and Access 中 App Store Connect API 入口创建的密钥;App Store Server API、Advanced Commerce API 和 External Purchase Server API 使用 In-App Purchase 入口的密钥。两者都可能下载为 .p8 文件,但服务入口、授权范围和调用端点不同,不能仅凭文件扩展名判断用途。

fastlane 使用 API Key 上传 TestFlight 时,应该先检查什么?

先确认 fastlane 实际读取的是哪组 key_id、issuer_id 和私钥内容,再检查 key_filepath、key_content、JSON 文件及环境变量是否在当前 Runner 中生效。随后用同一份配置执行最小认证请求,再运行一次 pilot 或 upload_to_testflight;不要只根据 JWT 文件生成成功就判定认证链正常。

本地成功、远程 Mac 返回 401,如何判断是环境问题?

把同一个脱敏配置分别放入本地终端、SSH 会话和 CI Runner,记录请求地址、状态码、Apple 错误码、请求 ID、执行用户、当前目录与系统时间。若只有无人值守任务失败,优先检查环境变量作用域、文件权限、换行符、工作目录和时钟;若三个入口都失败,再回到密钥和账号权限层排查。

用专属远程 Mac,稳定完成构建与发布

KVMFLUX 提供独享物理 Mac,凭据、密钥串与构建环境可持续保留,减少认证配置反复出错。 通过 SSH 或 VNC 连接远程 macOS,按需完成签名、打包、测试与上传任务。 按日、周、月或季灵活租用,最低按日计费,适合独立开发者和持续集成节点。 选择区域与周期后,KVMFLUX 通常几分钟内交付连接信息,让你尽快恢复发布流程。

Mac Mini M4 · 16GB / 256GB
按天$19.3 /天
按周$52.2 /周
按月$96.7 /月
按季$263 /季