做 macOS 应用时,你可能遇到过一种很像"系统失忆"的现象:
- 开发阶段已经允许了辅助功能、屏幕录制或麦克风权限;
- 打包安装后,应用又要求授权;
- 修改一次 Bundle ID,系统设置里原来的开关还在,新版本却不能使用;
- 应用名称和图标明明没变,macOS 却像看到了一款全新的应用。
这并不是 macOS 单纯按应用名称识别软件,也不是每次打包都会随机生成一个神秘密钥。真正的原因是:macOS 会结合 Bundle ID 和代码签名来判断"这是不是之前那一个应用"。当这个身份发生变化,旧权限不会自动转移给新身份。
本文从这个现象出发,解释 Bundle ID、Team ID、签名证书和 TCC 分别是什么,以及 Electron 应用应该怎样配置,才能避免每次打包都重新授权。
先说结论:文件名不是应用的身份证
假设桌面上有两个文件,都叫 Mimo.app,图标也完全相同。对用户来说,它们看起来可能是同一个应用;对 macOS 来说却未必。
系统判断应用身份时,关心的是这些信息:
- 应用声明的 Bundle ID;
- 应用由谁签名,也就是签名证书背后的 Team ID;
- 当前二进制是否满足它的 Designated Requirement(指定要求);
- 某些场景下,还会涉及应用路径、签名状态和权限类型。
可以把它粗略理解成:
应用名称像昵称,Bundle ID 像账号,代码签名像经过验证的账号归属。
这个类比不是完整的安全模型,但足以解释为什么"改个名字"通常没事,而"换 Bundle ID 或换签名"可能让权限全部失效。
Bundle ID、Team ID 和证书不是一回事
这几个概念经常被统称为"应用 ID"或"密钥",结果越说越乱。它们承担的职责并不相同。
1. Bundle ID:应用声明自己是谁
Bundle ID 通常采用反向域名格式:
text
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 信息中查看。
例如:
text
ABCDE12345
同一个开发团队签发的证书通常会关联相同的 Team ID。它回答的是"这个应用由哪个 Apple 开发团队负责",不是"这是哪个应用"。
3. 签名证书:用来证明发布者身份
面向 App Store 外分发 macOS 应用时,常用证书是:
text
Developer ID Application: Your Company Name (ABCDE12345)
证书本身是公开身份信息与公钥;真正需要妥善保管的是钥匙串中的私钥。打包工具使用私钥完成签名,其他人再用证书链验证签名是否可信。
因此,平时所说的"签名证书"并不等于把某个密钥字符串填进配置文件。私钥通常保存在 macOS 钥匙串中,CI 环境则通过加密的 .p12 文件和密码导入。
4. Designated Requirement:系统怎样再次认出它
代码签名可以为应用形成一条指定要求。简化后,它可能表达类似这样的条件:
text
identifier "com.example.mimo"
and anchor apple generic
and certificate leaf[subject.OU] = "ABCDE12345"
实际表达式可能更复杂,但重点是:Bundle ID 和签名团队会共同参与应用身份判断。
你可以查看一个应用的指定要求:
bash
codesign -d -r- "/Applications/Mimo.app" 2>&1
TCC 为什么会让旧权限失效
macOS 使用 TCC(Transparency, Consent, and Control)管理隐私权限,例如:
- 辅助功能(Accessibility);
- 屏幕与系统音频录制(Screen Recording);
- 麦克风和摄像头;
- 自动化控制其他应用(Apple Events);
- 通讯录、日历、提醒事项等数据。
用户点击"允许"时,系统保存的并不是一句简单的:
text
允许 Mimo.app
它会把授权与能够识别该客户端的信息关联起来,其中可能包括 Bundle ID 和代码签名要求。下一次应用访问受保护资源时,TCC 会重新检查请求者是否仍然符合原来的身份条件。
于是就会出现下面的情况:
text
旧版本:com.example.mimo + Team A 的有效签名
新版本:com.example.mimo.dev + Team A 的有效签名
Bundle ID 变了,系统不会假设新应用可以继承旧应用的屏幕录制权限。
再比如:
text
旧版本:com.example.mimo + Team A
新版本:com.example.mimo + 临时签名或 Team B
即使 Bundle ID 一样,签名身份不一致,原来的授权也可能无法匹配。
这其实是一项必要的安全设计。如果权限只看文件名,恶意程序只要把自己改名为 WeChat.app,就可能继承微信已经获得的麦克风或辅助功能权限。
哪些操作容易让系统认为它是新应用
修改 Bundle ID
这是最直接的身份变化。下面两个 ID 对 TCC 来说是两个客户端:
text
com.example.mimo
com.example.mimo.desktop
开发版和正式版使用不同 ID
有些项目会故意区分环境:
text
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:由项目确定,必要时在开发者后台注册
先为产品确定一个稳定命名,例如:
text
com.yourcompany.product
如果应用使用 Apple 服务,可以进入:
text
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 签发
常见获取方式有两种:
- 使用 Xcode 管理证书;
- 在 Certificates, Identifiers & Profiles 中创建
Developer ID Application证书。
创建证书时通常需要本机生成 CSR(Certificate Signing Request)。下载证书并安装后,它会与生成 CSR 时创建的私钥配对,出现在"钥匙串访问"中。
检查本机可用于代码签名的身份:
bash
security find-identity -v -p codesigning
如果输出中只有证书、没有对应私钥,打包工具仍然无法签名。钥匙串里证书左侧能够展开并看到私钥,才表示这台机器持有完整签名身份。
Electron 应用怎样保持身份稳定
下面是一个 electron-builder 配置示例:
json
{
"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
<?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 越多并不代表能力越强,反而会扩大安全面并增加公证排查难度。
如何确认两个包是不是同一个身份
先查看基本签名信息:
bash
codesign -dv --verbose=4 "/Applications/Mimo.app" 2>&1
重点关注:
text
Identifier
TeamIdentifier
Authority
CDHash
其中 CDHash 会随着代码内容变化,这是正常的,不能仅凭 CDHash 不同就认定应用身份一定改变。更应该对比 Identifier、TeamIdentifier、证书链和 designated requirement。
查看指定要求:
bash
codesign -d -r- "/Applications/Mimo.app" 2>&1
验证签名完整性:
bash
codesign --verify --deep --strict --verbose=2 "/Applications/Mimo.app"
检查 Gatekeeper 评估结果:
bash
spctl --assess --type execute --verbose=4 "/Applications/Mimo.app"
查看应用最终写入的 Bundle ID:
bash
/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \
"/Applications/Mimo.app/Contents/Info.plist"
如果本机包与 CI 包表现不同,把这些输出保存下来逐项对比,通常比反复打开系统设置更快。
tccutil reset 能做什么,不能做什么
调试时可以重置某类权限:
bash
tccutil reset Accessibility com.example.mimo
tccutil reset ScreenCapture com.example.mimo
tccutil reset Microphone com.example.mimo
也可以重置某一服务的全部记录:
bash
tccutil reset ScreenCapture
但要注意:
tccutil reset只能删除已有授权决定,不能代替用户点击"允许",也不能把旧 Bundle ID 的权限迁移给新 Bundle ID。
如果你的目标是验证首次授权流程,重置很有用;如果你的目标是让每个新构建自动继承权限,它解决不了身份不稳定的问题。
一份实用排查清单
遇到"打包后权限突然失效"时,可以按下面的顺序检查:
- 开发包和发行包的
CFBundleIdentifier是否一致; codesign输出中的TeamIdentifier是否一致;- 是否始终使用预期的 Developer ID Application 身份;
- 主应用和 Electron Helper 是否都通过签名验证;
- 打包后是否又修改了
.app内部文件; - 权限描述是否出现在最终包的
Info.plist,而不只是源代码配置中; - 当前请求权限的进程到底是正式应用、Electron、终端还是 IDE;
- 系统设置中的旧条目是否对应另一个 Bundle ID 或另一个签名身份;
- CI 是否导入了正确证书及其私钥;
- 是否把"重置权限"误认为"授予权限"。
最后:稳定的不是文件,而是发布身份
macOS 权限看起来像是授予一个 .app 文件,实际上它依赖的是一个能够持续验证的应用身份。
想让升级后的版本继续被系统识别,最重要的是:
- 尽早确定并固定 Bundle ID;
- 使用同一 Apple Developer Team 的稳定签名流程;
- 确保所有嵌套组件正确签名;
- 把开发版和正式版当作两个身份时,就接受它们各自授权;
- 在 CI 和本机都检查最终产物,而不是只检查配置文件。
所以,下次看到"换了一个 ID,之前权限全没了",不要把它理解为 macOS 随机生成了一个新密钥。更准确的说法是:应用向系统出示的身份证明发生了变化,系统为了安全,不会让新的身份自动继承旧身份的隐私权限。