为什么 macOS 应用一换 Bundle ID,之前授予的权限就全失效了?

19 阅读10分钟

做 macOS 应用时,你可能遇到过一种很像“系统失忆”的现象:

  • 开发阶段已经允许了辅助功能、屏幕录制或麦克风权限;
  • 打包安装后,应用又要求授权;
  • 修改一次 Bundle ID,系统设置里原来的开关还在,新版本却不能使用;
  • 应用名称和图标明明没变,macOS 却像看到了一款全新的应用。

这并不是 macOS 单纯按应用名称识别软件,也不是每次打包都会随机生成一个神秘密钥。真正的原因是:macOS 会结合 Bundle ID 和代码签名来判断“这是不是之前那一个应用”。当这个身份发生变化,旧权限不会自动转移给新身份。

本文从这个现象出发,解释 Bundle ID、Team ID、签名证书和 TCC 分别是什么,以及 Electron 应用应该怎样配置,才能避免每次打包都重新授权。

先说结论:文件名不是应用的身份证

假设桌面上有两个文件,都叫 Mimo.app,图标也完全相同。对用户来说,它们看起来可能是同一个应用;对 macOS 来说却未必。

系统判断应用身份时,关心的是这些信息:

  1. 应用声明的 Bundle ID
  2. 应用由谁签名,也就是签名证书背后的 Team ID
  3. 当前二进制是否满足它的 Designated Requirement(指定要求)
  4. 某些场景下,还会涉及应用路径、签名状态和权限类型。

可以把它粗略理解成:

应用名称像昵称,Bundle ID 像账号,代码签名像经过验证的账号归属。

这个类比不是完整的安全模型,但足以解释为什么“改个名字”通常没事,而“换 Bundle ID 或换签名”可能让权限全部失效。

Bundle ID、Team ID 和证书不是一回事

这几个概念经常被统称为“应用 ID”或“密钥”,结果越说越乱。它们承担的职责并不相同。

1. Bundle ID:应用声明自己是谁

Bundle ID 通常采用反向域名格式:

com.example.mimo

它位于应用包的 Info.plist 中,对应 CFBundleIdentifier。在 Electron Builder 中,通常由 build.appId 决定。

Bundle ID 不是 Apple 随机发给你的秘密,也不是密码。你需要为应用选择一个长期稳定、在自己组织命名空间内唯一的字符串。使用推送、iCloud 等 Apple 能力时,还要在 Apple Developer 后台注册相应 Identifier。

2. Team ID:签名者属于哪个开发团队

Team ID 是 Apple 为开发者团队分配的标识。加入 Apple Developer Program 后,可以在 Apple Developer 账户的 Membership 信息中查看。

例如:

ABCDE12345

同一个开发团队签发的证书通常会关联相同的 Team ID。它回答的是“这个应用由哪个 Apple 开发团队负责”,不是“这是哪个应用”。

3. 签名证书:用来证明发布者身份

面向 App Store 外分发 macOS 应用时,常用证书是:

Developer ID Application: Your Company Name (ABCDE12345)

证书本身是公开身份信息与公钥;真正需要妥善保管的是钥匙串中的私钥。打包工具使用私钥完成签名,其他人再用证书链验证签名是否可信。

因此,平时所说的“签名证书”并不等于把某个密钥字符串填进配置文件。私钥通常保存在 macOS 钥匙串中,CI 环境则通过加密的 .p12 文件和密码导入。

4. Designated Requirement:系统怎样再次认出它

代码签名可以为应用形成一条指定要求。简化后,它可能表达类似这样的条件:

identifier "com.example.mimo"
and anchor apple generic
and certificate leaf[subject.OU] = "ABCDE12345"

实际表达式可能更复杂,但重点是:Bundle ID 和签名团队会共同参与应用身份判断。

你可以查看一个应用的指定要求:

codesign -d -r- "/Applications/Mimo.app" 2>&1

TCC 为什么会让旧权限失效

macOS 使用 TCC(Transparency, Consent, and Control)管理隐私权限,例如:

  • 辅助功能(Accessibility);
  • 屏幕与系统音频录制(Screen Recording);
  • 麦克风和摄像头;
  • 自动化控制其他应用(Apple Events);
  • 通讯录、日历、提醒事项等数据。

用户点击“允许”时,系统保存的并不是一句简单的:

允许 Mimo.app

它会把授权与能够识别该客户端的信息关联起来,其中可能包括 Bundle ID 和代码签名要求。下一次应用访问受保护资源时,TCC 会重新检查请求者是否仍然符合原来的身份条件。

于是就会出现下面的情况:

旧版本:com.example.mimo + Team A 的有效签名
新版本:com.example.mimo.dev + Team A 的有效签名

Bundle ID 变了,系统不会假设新应用可以继承旧应用的屏幕录制权限。

再比如:

旧版本:com.example.mimo + Team A
新版本:com.example.mimo + 临时签名或 Team B

即使 Bundle ID 一样,签名身份不一致,原来的授权也可能无法匹配。

这其实是一项必要的安全设计。如果权限只看文件名,恶意程序只要把自己改名为 WeChat.app,就可能继承微信已经获得的麦克风或辅助功能权限。

哪些操作容易让系统认为它是新应用

修改 Bundle ID

这是最直接的身份变化。下面两个 ID 对 TCC 来说是两个客户端:

com.example.mimo
com.example.mimo.desktop

开发版和正式版使用不同 ID

有些项目会故意区分环境:

com.example.mimo.dev
com.example.mimo

这没有问题,但它们本来就应该拥有两套独立权限。不要期待开发版的授权自动继承给正式版。

开发阶段没有稳定签名

本地构建可能使用 ad-hoc 签名、Apple Development 证书,甚至由不同工具重新签名;正式包则使用 Developer ID Application。两者的代码要求不一定相同。

尤其在 Electron 开发模式中,实际发起权限请求的可能是 Electron、终端或 IDE 启动的进程,而不是最终安装到 /Applications 的正式应用。因此开发模式下授权成功,不代表发行包已经获得相同权限。

CI 与本机使用了不同团队或证书

正常续签同一团队下的 Developer ID 证书,不一定会让应用变成完全无关的身份;但如果团队改变、签名丢失、嵌套组件签名不一致,代码要求就可能无法继续匹配。

打包后又修改应用内容

签名完成后继续替换 Framework、Helper、资源或可执行文件,会破坏签名。系统可能拒绝运行,也可能让某些能力和授权表现异常。

这些 ID 和证书到底去哪里获取

Bundle ID:由项目确定,必要时在开发者后台注册

先为产品确定一个稳定命名,例如:

com.yourcompany.product

如果应用使用 Apple 服务,可以进入:

Apple Developer → Certificates, Identifiers & Profiles → Identifiers

创建 App ID,并让它与项目中的 Bundle ID 保持一致。

Team ID:在 Apple Developer Membership 中查看

登录 Apple Developer 账户后,在 Membership Details 中可以看到 Team ID。Xcode 的 Settings → Accounts 里也能看到已登录团队。

Developer ID Application 证书:由 Apple Developer 签发

常见获取方式有两种:

  1. 使用 Xcode 管理证书;
  2. 在 Certificates, Identifiers & Profiles 中创建 Developer ID Application 证书。

创建证书时通常需要本机生成 CSR(Certificate Signing Request)。下载证书并安装后,它会与生成 CSR 时创建的私钥配对,出现在“钥匙串访问”中。

检查本机可用于代码签名的身份:

security find-identity -v -p codesigning

如果输出中只有证书、没有对应私钥,打包工具仍然无法签名。钥匙串里证书左侧能够展开并看到私钥,才表示这台机器持有完整签名身份。

Electron 应用怎样保持身份稳定

下面是一个 electron-builder 配置示例:

{
  "name": "mimo-desktop",
  "version": "1.0.0",
  "build": {
    "appId": "com.example.mimo",
    "productName": "Mimo",
    "mac": {
      "target": ["dmg", "zip"],
      "identity": "Developer ID Application: Example Company (ABCDE12345)",
      "hardenedRuntime": true,
      "entitlements": "build/entitlements.mac.plist",
      "entitlementsInherit": "build/entitlements.mac.inherit.plist",
      "extendInfo": {
        "NSMicrophoneUsageDescription": "用于在通话中采集麦克风声音",
        "NSCameraUsageDescription": "用于在视频通话中使用摄像头"
      }
    }
  }
}

这里有几个关键点:

  • appId 一旦发布就尽量不要修改;
  • productName 可以调整,它主要影响用户看到的名称;
  • 正式构建始终使用同一个 Apple Developer Team 的分发身份;
  • 主应用、Helper 和 Framework 都要正确签名;
  • 麦克风、摄像头等权限需要相应的 Usage Description;
  • Usage Description 只负责向用户解释用途,并不会直接授予权限;
  • 辅助功能、屏幕录制等权限仍需要用户在系统设置中明确授权。

一个最小的 entitlements 文件可能是:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "https://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
</dict>
</plist>

具体 entitlement 应根据 Electron 版本和应用能力选择,不要为了“避免报错”复制一大包不需要的权限。entitlement 越多并不代表能力越强,反而会扩大安全面并增加公证排查难度。

如何确认两个包是不是同一个身份

先查看基本签名信息:

codesign -dv --verbose=4 "/Applications/Mimo.app" 2>&1

重点关注:

Identifier
TeamIdentifier
Authority
CDHash

其中 CDHash 会随着代码内容变化,这是正常的,不能仅凭 CDHash 不同就认定应用身份一定改变。更应该对比 Identifier、TeamIdentifier、证书链和 designated requirement。

查看指定要求:

codesign -d -r- "/Applications/Mimo.app" 2>&1

验证签名完整性:

codesign --verify --deep --strict --verbose=2 "/Applications/Mimo.app"

检查 Gatekeeper 评估结果:

spctl --assess --type execute --verbose=4 "/Applications/Mimo.app"

查看应用最终写入的 Bundle ID:

/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
  "/Applications/Mimo.app/Contents/Info.plist"

如果本机包与 CI 包表现不同,把这些输出保存下来逐项对比,通常比反复打开系统设置更快。

tccutil reset 能做什么,不能做什么

调试时可以重置某类权限:

tccutil reset Accessibility com.example.mimo
tccutil reset ScreenCapture com.example.mimo
tccutil reset Microphone com.example.mimo

也可以重置某一服务的全部记录:

tccutil reset ScreenCapture

但要注意:

tccutil reset 只能删除已有授权决定,不能代替用户点击“允许”,也不能把旧 Bundle ID 的权限迁移给新 Bundle ID。

如果你的目标是验证首次授权流程,重置很有用;如果你的目标是让每个新构建自动继承权限,它解决不了身份不稳定的问题。

一份实用排查清单

遇到“打包后权限突然失效”时,可以按下面的顺序检查:

  1. 开发包和发行包的 CFBundleIdentifier 是否一致;
  2. codesign 输出中的 TeamIdentifier 是否一致;
  3. 是否始终使用预期的 Developer ID Application 身份;
  4. 主应用和 Electron Helper 是否都通过签名验证;
  5. 打包后是否又修改了 .app 内部文件;
  6. 权限描述是否出现在最终包的 Info.plist,而不只是源代码配置中;
  7. 当前请求权限的进程到底是正式应用、Electron、终端还是 IDE;
  8. 系统设置中的旧条目是否对应另一个 Bundle ID 或另一个签名身份;
  9. CI 是否导入了正确证书及其私钥;
  10. 是否把“重置权限”误认为“授予权限”。

最后:稳定的不是文件,而是发布身份

macOS 权限看起来像是授予一个 .app 文件,实际上它依赖的是一个能够持续验证的应用身份。

想让升级后的版本继续被系统识别,最重要的是:

  • 尽早确定并固定 Bundle ID;
  • 使用同一 Apple Developer Team 的稳定签名流程;
  • 确保所有嵌套组件正确签名;
  • 把开发版和正式版当作两个身份时,就接受它们各自授权;
  • 在 CI 和本机都检查最终产物,而不是只检查配置文件。

所以,下次看到“换了一个 ID,之前权限全没了”,不要把它理解为 macOS 随机生成了一个新密钥。更准确的说法是:应用向系统出示的身份证明发生了变化,系统为了安全,不会让新的身份自动继承旧身份的隐私权限。