本文目标:帮助你把本地的 Electron 应用打包成 macOS 的
.dmg,并做到打开不再被 Gatekeeper 拦截(不再提示“来自身份不明的开发者/无法验证是否含有恶意软件”)。适用对象:个人开发者 & 小团队。
🧩 一、问题场景
当你满心欢喜地将精心开发的 Electron 应用打包分发给用户,却接到反馈:在 macOS 上无法打开,系统弹窗冷冰冰地提示“无法验证开发者”,文件被直接移入废纸篓。
如果这个场景让你感同身受,那么你正遭遇 macOS 强大的 Gatekeeper 安全机制。别担心,这不是你的代码有问题,而是应用缺少一张合法的“身份证”——苹果的开发者签名。本指南将带你一步步为你的 Electron 应用完成代码签名,彻底告别“不安全软件”的提示。
🚀 二、从 0 到 1 的整体流程(必做)
| 阶段 | 目标 | 核心工具 | 产出 |
|---|---|---|---|
| 1. 证书准备 | 让 Apple 识别你的身份 | Apple Developer、Keychain | Developer ID Application 证书 |
| 2. 签名 | 给 .app 加数字签名 | electron-builder/codesign | 已签名的 .app/.dmg |
| 3. 公证 | 让 Apple 云端扫描并背书 | notarytool | Accepted 的公证结果 |
| 4. staple | 把公证票据装订回产物 | xcrun stapler | 可离线验证的 .app/.dmg |
| 5. 验证 | 本地三板斧校验 | codesign/spctl/stapler | 放心分发 |
🧱 三、准备工作
1) Apple Developer 账号
- 开通 developer.apple.com 开发者计划($99/年)。
- 建议以组织(Organization)形式创建,便于分权协作。
2) 正确的证书类型
不要用
Mac Development/Apple Development。它们只用于调试。
- ✅ 需要:
Developer ID Application(给.app/.dmg签名用) - (可选):
Developer ID Installer(制作.pkg安装器才用)
可能的问题:
- 创建页里没有 "Developer ID Application" → 你不是 Account Holder/Admin 或选错了 Team。让 Account Holder 升级角色或代为创建。
3) 获取 Developer ID Application 证书的详细步骤
步骤 3.1:在本地生成证书签名请求(CSR)
- 打开 macOS 的 钥匙串访问 (Keychain Access)
- 菜单栏选择:钥匙串访问 → 证书助理 → 从证书颁发机构请求证书...
- 填写信息:
- 用户电子邮件地址:填写你的 Apple ID 邮箱(例如:
[email protected]) - 常用名称:填写你的名字或公司名(例如:
Example Developer) - CA 电子邮件地址:留空
- 选择:存储到磁盘
- 勾选:让我指定密钥对信息
- 用户电子邮件地址:填写你的 Apple ID 邮箱(例如:
- 点击 继续
- 在密钥对信息中:
- 密钥大小:2048 位
- 算法:RSA
- 保存 CSR 文件(例如:
CertificateSigningRequest.certSigningRequest)到桌面或其他位置
⚠️ 注意:生成 CSR 后,钥匙串中会自动生成一个对应的私钥,请勿删除!证书下载后需要与这个私钥配对使用。
步骤 3.2:在 Apple Developer 网站申请证书
- 登录 Apple Developer
- 进入:Certificates, Identifiers & Profiles → Certificates
- 点击左上角 + 按钮创建新证书
- 选择证书类型:
- 滚动到 Production 区域
- 选择 Developer ID Application
- 点击 Continue
- 上传 CSR:
- 点击 Choose File
- 选择刚才生成的
.certSigningRequest文件 - 点击 Continue
- 下载证书:
- 点击 Download 下载
.cer文件(例如:developerID_application.cer)
- 点击 Download 下载
步骤 3.3:将证书导入钥匙串
- 双击下载的
.cer文件 - 系统会自动打开钥匙串访问并导入证书
- 验证导入成功:
- 打开 钥匙串访问
- 选择 登录 钥匙串
- 在 我的证书 分类中查找
- 应该能看到类似
Developer ID Application: Your Name (TEAM_ID)的证书 - 展开证书,确认下方有对应的私钥(🔑 图标)
- 验证证书可用性:
1# 列出所有可用的签名身份 2security find-identity -v -p codesigning 3 4# 应该能看到类似输出: 5# 1) ABCD1234EFGH5678IJKL9012MNOP3456QRST7890 "Developer ID Application: Your Name (TEAM_ID)" 6
⚠️ 常见问题:
- 证书导入后没有私钥:说明 CSR 不是在本机生成的,或者私钥被删除了。需要重新生成 CSR 并申请新证书。
- 无法在钥匙串中找到证书:确认导入到了 登录 钥匙串而不是 系统 钥匙串。
4) App Store Connect API Key(用于公证)
步骤 4.1:创建 API Key
- 登录 App Store Connect
- 导航路径:Users and Access → Keys → App Store Connect API
- 点击页面中的 + 按钮(或 Generate API Key)
- 填写信息:
- Name:给 Key 起个名字(例如:
Notarization Key或CI/CD Key) - Access:选择 Developer(公证只需要 Developer 权限)
- Name:给 Key 起个名字(例如:
- 点击 Generate
🔐 权限说明:
- Admin:最高权限,可管理所有内容
- Developer:可提交公证请求(推荐用于自动化)
- 如果只做公证,选择 Developer 即可
步骤 4.2:下载和保存 API Key
- 创建成功后,页面会显示:
- Key ID(例如:
KEYID12345)← 记录此值 - Issuer ID(页面顶部,例如:
00000000-1111-2222-3333-444444444444)← 记录此值 - Download API Key 按钮
- Key ID(例如:
- 立即下载
.p8文件:- 点击 Download API Key
- 文件名格式:
AuthKey_XXXXXXXX.p8(例如:AuthKey_ABCD1234.p8) - ⚠️ 此文件只能下载一次! 下载后妥善保管,丢失需要重新创建 Key
- 保存到项目目录:
1# 建议的项目结构 2your-electron-app/ 3├── build/ 4│ ├── AuthKey_ABCD1234.p8 # API Key 文件 5│ ├── entitlements.mac.plist # 权限配置 6│ └── afterSign.js # 可选:自动补签脚本 7├── electron-builder.yml 8└── package.json 9
- 安全建议:
- 将
.p8文件添加到.gitignore,避免提交到 Git - 在 CI/CD 中使用环境变量或加密存储
- 将
1# .gitignore 示例 2build/*.p8 3*.p8 4
步骤 4.3:记录三项关键信息(mock 示例)
完成后,你应该拥有以下三项信息:
| 项目 | 说明 | Mock 示例 |
|---|---|---|
| API Key 文件 | .p8 文件路径 | build/AuthKey_ABCD1234.p8 |
| Key ID | 在 App Store Connect 中显示 | KEYID12345 |
| Issuer ID | 在 App Store Connect 顶部显示 | 00000000-1111-2222-3333-444444444444 |
可能的问题:
- App Store Connect看不到 Keys 菜单 → 你不是 Account Holder;请让 Account Holder 创建并分享
.p8/KeyID/IssuerID给你。 - .p8 文件丢失 → 无法重新下载,只能删除旧 Key 并创建新的。
- 权限不足 → 如果公证失败提示权限问题,尝试使用 Admin 权限的 Key。
⚙️ 四、项目配置(使用 electron-builder)
1) 完整的项目结构
1your-electron-app/ 2├── build/ # 构建资源目录 3│ ├── AuthKey_ABCD1234.p8 # ⚠️ API Key(不要提交到 Git) 4│ ├── entitlements.mac.plist # macOS 权限配置 5│ ├── afterSign.js # 可选:签名后处理脚本 6│ └── icon.icns # 应用图标 7├── src/ # 源代码 8│ ├── main.js # Electron 主进程 9│ └── renderer/ # 渲染进程 10├── scripts/ 11│ └── check-sign.sh # 签名自检脚本 12├── .env # ⚠️ 环境变量(不要提交到 Git) 13├── .gitignore 14├── electron-builder.yml # electron-builder 配置 15└── package.json 16
2) package.json 配置(完整示例)
1{ 2 "name": "my-electron-app", 3 "version": "1.0.0", 4 "description": "My Electron App", 5 "main": "src/main.js", 6 "scripts": { 7 "start": "electron .", 8 "build": "electron-builder", 9 "build:mac": "electron-builder --mac", 10 "build:mac:dmg": "electron-builder --mac dmg", 11 "build:dir": "electron-builder --mac --dir", 12 "pack": "electron-builder --dir", 13 "dist": "electron-builder", 14 "check-sign": "bash scripts/check-sign.sh dist/mac/MyApp.app" 15 }, 16 "build": { 17 "appId": "com.example.myapp", 18 "productName": "MyApp", 19 "extends": "electron-builder.yml" 20 }, 21 "devDependencies": { 22 "electron": "^28.0.0", 23 "electron-builder": "^24.9.1" 24 }, 25 "author": "Your Name", 26 "license": "MIT" 27} 28
💡 脚本说明:
build:mac:构建 macOS 版本(包括 dmg 和 zip)build:mac:dmg:只构建 dmgbuild:dir:不打包,只构建目录(用于测试签名)check-sign:运行签名自检脚本
3) electron-builder.yml 配置(三种方式)
方式 A:直接硬编码(适合个人本地开发)
electron-builder.yml(mock 示例)
1appId: com.example.myapp 2productName: MyApp 3 4mac: 5 category: public.app-category.productivity 6 target: 7 - dmg 8 hardenedRuntime: true 9 gatekeeperAssess: false 10 entitlements: build/entitlements.mac.plist 11 entitlementsInherit: build/entitlements.mac.plist 12 notarize: 13 tool: "notarytool" 14 appleApiKey: "build/AuthKey_ABCD1234.p8" # mock:你的 .p8 文件路径 15 appleApiKeyId: "KEYID12345" # mock:你的 Key ID 16 appleApiIssuer: "00000000-1111-2222-3333-444444444444" # mock:你的 Issuer ID 17 18dmg: 19 title: "${productName} ${version}" 20 icon: "build/icon.icns" 21 background: "build/background.png" # 可选:DMG 背景图 22 window: 23 width: 540 24 height: 380 25 contents: 26 - x: 140 27 y: 200 28 type: "file" 29 - x: 400 30 y: 200 31 type: "link" 32 path: "/Applications" 33
⚠️ 注意:此方式会把敏感信息写在配置文件中,不要提交到公开仓库!
方式 B:使用环境变量(推荐用于 CI/CD)
electron-builder.yml
1appId: com.example.myapp 2productName: MyApp 3 4mac: 5 category: public.app-category.productivity 6 target: 7 - dmg 8 hardenedRuntime: true 9 entitlements: build/entitlements.mac.plist 10 entitlementsInherit: build/entitlements.mac.plist 11 notarize: 12 tool: "notarytool" 13 # 留空,通过环境变量传递 14
.env(本地开发,不要提交到 Git)
1# Apple Developer 证书相关 2APPLE_ID=[email protected] 3APPLE_ID_PASSWORD=your-app-specific-password 4 5# App Store Connect API Key(推荐方式) 6APPLE_API_KEY=build/AuthKey_ABCD1234.p8 7APPLE_API_KEY_ID=KEYID12345 8APPLE_API_ISSUER=00000000-1111-2222-3333-444444444444 9 10# 或者使用 base64 编码的 .p8 内容(CI/CD 推荐) 11# APPLE_API_KEY_BASE64=<base64-encoded-content> 12
使用环境变量的构建命令
1# 方式 1:使用 dotenv 2npm install dotenv-cli --save-dev 3npx dotenv electron-builder --mac dmg 4 5# 方式 2:直接导出环境变量 6export APPLE_API_KEY_ID="KEYID12345" 7export APPLE_API_ISSUER="00000000-1111-2222-3333-444444444444" 8export APPLE_API_KEY="build/AuthKey_ABCD1234.p8" 9npm run build:mac:dmg 10
方式 C:使用 electron-builder 的环境变量自动识别
electron-builder 会自动读取以下环境变量:
| 环境变量 | 说明 | Mock 示例 |
|---|---|---|
| APPLE_API_KEY | API Key 文件路径或 base64 内容 | build/AuthKey_ABCD1234.p8 |
| APPLE_API_KEY_ID | Key ID | KEYID12345 |
| APPLE_API_ISSUER | Issuer ID | 00000000-1111-2222-3333-444444444444 |
| APPLE_ID | Apple ID 邮箱(旧方式) | [email protected] |
| APPLE_APP_SPECIFIC_PASSWORD | App-specific password(旧方式) | xxxx-xxxx-xxxx-xxxx |
💡 推荐:在 CI/CD 中使用环境变量方式,避免敏感信息泄露。
build/entitlements.mac.plist(建议最小集)
1<?xml version="1.0" encoding="UTF-8"?> 2<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> 3<plist version="1.0"> 4 <dict> 5 <key>com.apple.security.cs.allow-jit</key><true/> 6 <key>com.apple.security.cs.allow-unsigned-executable-memory</key><true/> 7 <key>com.apple.security.cs.disable-library-validation</key><true/> 8 <key>com.apple.security.app-sandbox</key><false/> 9 <key>com.apple.security.network.client</key><true/> 10 </dict> 11</plist> 12
可能的问题:
hardenedRuntime未启用会导致公证失败。entitlements缺项(JIT/unsigned-executable-memory)会导致 Electron 无法正常运行或公证失败。
4) CI/CD 集成示例
GitHub Actions 配置示例
.github/workflows/build-macos.yml
1name: Build macOS App 2 3on: 4 push: 5 branches: [main] 6 tags: ['v*'] 7 pull_request: 8 branches: [main] 9 10jobs: 11 build: 12 runs-on: macos-latest 13 14 steps: 15 - name: Checkout code 16 uses: actions/checkout@v4 17 18 - name: Setup Node.js 19 uses: actions/setup-node@v4 20 with: 21 node-version: '20' 22 23 - name: Install dependencies 24 run: npm ci 25 26 # 准备签名证书(方式1:使用 .p12 文件) 27 - name: Import signing certificate 28 env: 29 CERTIFICATE_BASE64: ${{ secrets.MACOS_CERTIFICATE_BASE64 }} 30 CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} 31 run: | 32 # 创建临时 keychain 33 KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db 34 KEYCHAIN_PASSWORD=$(openssl rand -base64 32) 35 36 security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 37 security set-keychain-settings -lut 21600 $KEYCHAIN_PATH 38 security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 39 40 # 导入证书 41 echo $CERTIFICATE_BASE64 | base64 --decode > certificate.p12 42 security import certificate.p12 -k $KEYCHAIN_PATH -P "$CERTIFICATE_PASSWORD" -T /usr/bin/codesign 43 security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH 44 45 # 设置为默认 keychain 46 security list-keychain -d user -s $KEYCHAIN_PATH 47 48 # 清理 49 rm -f certificate.p12 50 51 # 准备 API Key 52 - name: Prepare API Key 53 env: 54 APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }} 55 run: | 56 mkdir -p build 57 echo $APPLE_API_KEY_BASE64 | base64 --decode > build/AuthKey.p8 58 59 # 构建应用 60 - name: Build and notarize 61 env: 62 APPLE_API_KEY: build/AuthKey.p8 63 APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }} 64 APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }} 65 run: npm run build:mac:dmg 66 67 # 上传构建产物 68 - name: Upload artifacts 69 uses: actions/upload-artifact@v4 70 with: 71 name: macos-dmg 72 path: dist/*.dmg 73
准备 GitHub Secrets
在 GitHub 仓库设置中添加以下 Secrets(Settings → Secrets and variables → Actions):
| Secret Name | 说明 | 如何获取 |
|---|---|---|
| APPLE_API_KEY_BASE64 | .p8 文件的 base64 编码 | base64 -i AuthKey_ABCD1234.p8 |
| APPLE_API_KEY_ID | Key ID | 从 App Store Connect 复制 |
| APPLE_API_ISSUER | Issuer ID | 从 App Store Connect 复制 |
| MACOS_CERTIFICATE_BASE64 | 证书 .p12 的 base64 | base64 -i certificate.p12 |
| MACOS_CERTIFICATE_PASSWORD | .p12 文件的密码 | 导出证书时设置的密码 |
如何导出 .p12 证书:
1# 1. 在钥匙串访问中找到证书 2# 2. 右键点击证书 → 导出 3# 3. 文件格式选择 "个人信息交换 (.p12)" 4# 4. 设置密码(用于 CI/CD) 5# 5. 保存后,转换为 base64: 6base64 -i certificate.p12 | pbcopy # 复制到剪贴板 7
🔐 五、安全管理最佳实践
1) .gitignore 配置(必做)
1# .gitignore 2# macOS 构建相关 3*.p8 4*.p12 5*.cer 6*.mobileprovision 7build/*.p8 8build/AuthKey_*.p8 9 10# 环境变量 11.env 12.env.local 13.env.production 14 15# 构建产物 16dist/ 17out/ 18release/ 19 20# 临时文件 21*.dmg 22*.zip 23*.pkg 24
2) 敏感信息管理建议
| 场景 | 推荐方案 | 不推荐 |
|---|---|---|
| 本地开发 | .env 文件 + .gitignore | 硬编码在配置文件中 |
| 团队协作 | 1Password / Bitwarden 等密码管理器 | 通过聊天工具传递 |
| CI/CD | GitHub Secrets / GitLab Variables | 明文存储在配置文件 |
| 多环境 | 不同的 API Key(dev/prod) | 共用同一个 Key |
3) API Key 权限最小化
1# 生产环境:只给 Developer 权限 2production_key: 3 access: Developer 4 scope: Notarization only 5 6# 开发环境:可以给更多权限方便调试 7development_key: 8 access: Developer 9 scope: Notarization + TestFlight 10
4) 证书过期管理
1# 检查证书有效期 2security find-identity -v -p codesigning | grep "Developer ID Application" 3openssl x509 -inform DER -in developerID_application.cer -noout -dates 4 5# 证书有效期通常为 5-7 年,提前 1 个月续签 6
5) CI/CD 安全检查清单
- ✅ 确保
.p8和.p12文件已添加到.gitignore - ✅ 使用 base64 编码存储在 CI Secrets 中
- ✅ 构建后自动清理临时 keychain
- ✅ 不在日志中打印敏感信息
- ✅ 使用短期有效的临时 keychain
- ✅ 限制 CI/CD 的访问权限(只给必要的人员)
🏗️ 六、打包与自动公证
1# 安装依赖(示例) 2npm i -D electron-builder 3 4# 一条龙:签名 + 提交公证 + staple 5npx electron-builder --mac dmg 6
公证状态查看(任选其一)
1# 查看历史 2xcrun notarytool history \ 3 --key build/AuthKey_ABCD1234.p8 \ 4 --key-id KEYID12345 \ 5 --issuer 00000000-1111-2222-3333-444444444444 6 7# 查看指定请求 8xcrun notarytool info <REQUEST_ID> \ 9 --key build/AuthKey_ABCD1234.p8 \ 10 --key-id KEYID12345 \ 11 --issuer 00000000-1111-2222-3333-444444444444 12
当状态为 Accepted 后,若未自动 staple:
1xcrun stapler staple "dist/mac/MyApp.app" 2xcrun stapler staple "dist/mac/MyApp.dmg" 3
🔎 七、公证前自检脚本(强烈推荐)
在提交公证前,先扫描 .app 内所有 Mach-O 文件并验证签名,可大幅提高一次通过率。
scripts/check-sign.sh
1#!/usr/bin/env bash 2set -euo pipefail 3 4APP="${1:-dist/mac/MyApp.app}" 5 6echo "== 1) 枚举 Mach-O 可执行体 ==" 7find "$APP" -type f -print0 \ 8| xargs -0 file \ 9| grep -E 'Mach-O|dynamically linked shared library|bundle with Mach-O' \ 10| sed 's/:.*$//' \ 11| tee /tmp/macho.list 12 13echo 14echo "== 2) 对每个 Mach-O 执行严格签名校验 ==" 15rc=0 16while IFS= read -r f; do 17 echo "-> $f" 18 if ! codesign --verify --strict --verbose=4 "$f" >/dev/null 2>&1; then 19 echo "⚠️ 未签名或签名无效: $f" 20 rc=1 21 fi 22done < /tmp/macho.list 23 24echo 25echo "== 3) 校验 .app 整体签名 ==" 26if ! codesign --verify --strict --deep --verbose=4 "$APP"; then 27 echo "⚠️ .app 整体签名校验失败" 28 rc=1 29fi 30 31echo 32echo "== 4) Gatekeeper 预检 ==" 33if ! spctl -a -vv "$APP"; then 34 echo "⚠️ Gatekeeper 预检未通过" 35 rc=1 36fi 37 38echo 39echo "== 5) 列出解包原生模块(高风险区) ==" 40if [ -d "$APP/Contents/Resources/app.asar.unpacked" ]; then 41 find "$APP/Contents/Resources/app.asar.unpacked" -type f \( -name "*.node" -o -name "*.dylib" -o -name "*.jnilib" -o -name "*.so" \) -print 42fi 43 44echo 45echo "== 6) 检查主可执行体是否启用 Hardened Runtime ==" 46MAIN_BIN="$APP/Contents/MacOS/$(defaults read "$APP/Contents/Info" CFBundleExecutable 2>/dev/null || echo '')" 47if [ -n "$MAIN_BIN" ] && ! codesign -d --verbose=4 "$MAIN_BIN" 2>&1 | grep -qi 'runtime'; then 48 echo "⚠️ 未检测到 hardened runtime 选项(需要 mac.hardenedRuntime: true 且使用 --options runtime)" 49 rc=1 50fi 51 52echo 53if [ "$rc" -ne 0 ]; then 54 echo "❌ 自检未通过,请根据上方提示修复后再提交公证" 55 exit 1 56else 57 echo "✅ 自检通过,建议开始 Notarization" 58fi 59
使用方式
1chmod +x scripts/check-sign.sh 2scripts/check-sign.sh dist/mac/MyApp.app 3
🛠️ 八、自动补签(afterSign.js,可选)
若第三方依赖解包出了 .dylib/.jnilib/.node 且未签名,可在 electron-builder.yml 中挂载:
1afterSign: build/afterSign.js 2
build/afterSign.js(简版示例)
1const { execFile } = require("child_process"); 2const fs = require("fs"); 3const path = require("path"); 4 5function run(cmd, args) { 6 return new Promise((resolve, reject) => { 7 execFile(cmd, args, (err, stdout, stderr) => { 8 if (err) return reject(new Error(stderr || stdout || err?.message)); 9 resolve(); 10 }); 11 }); 12} 13 14function walk(dir, exts, out) { 15 if (!fs.existsSync(dir)) return; 16 for (const name of fs.readdirSync(dir)) { 17 const p = path.join(dir, name); 18 const st = fs.lstatSync(p); 19 if (st.isSymbolicLink()) continue; 20 if (st.isDirectory()) walk(p, exts, out); 21 else if (exts.some(e => p.endsWith(e))) out.push(p); 22 } 23} 24 25module.exports = async (context) => { 26 if (context.electronPlatformName !== "darwin") return; 27 const appPath = path.join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`); 28 const entitlements = path.join(process.cwd(), "build/entitlements.mac.plist"); 29 const identity = "Developer ID Application"; 30 31 const targets = []; 32 walk(path.join(appPath, "Contents/Frameworks"), [".dylib", ".jnilib", ".node", ".so"], targets); 33 walk(path.join(appPath, "Contents/Resources", "app.asar.unpacked"), [".dylib", ".jnilib", ".node", ".so"], targets); 34 35 for (const f of targets) { 36 try { 37 // 已签名会通过;未签名会抛异常,进入 catch 重新签名 38 await run("codesign", ["--verify", "--strict", "--verbose=2", f]); 39 } catch { 40 console.log("🔧 重新签名:", f); 41 await run("codesign", ["--force", "--sign", identity, "--timestamp", "--options", "runtime", "--entitlements", entitlements, f]); 42 } 43 } 44}; 45
🧪 九、三板斧验证(公证后)
1codesign -dv --verbose=4 "dist/mac/MyApp.app" # Authority 应为 Developer ID Application: ... 2spctl -a -vv "dist/mac/MyApp.app" # 应为 accepted 3xcrun stapler validate "dist/mac/MyApp.dmg" # The validate action worked! 4
🔁 十、何时需要重新公证?
- 改动
.app内任何内容(包括图标.icns、Info.plist、资源、代码、Electron 版本等)→ ✅ 需要重新签名+公证。 - 只改
.dmg的外观/背景(不改.app)→ ❌ 无需重公证,但建议对新.dmg再次stapler staple。
🧱 十一、常见坑位速查表
| 现象/报错 | 核心原因 | 处理 |
|---|---|---|
| 看不到 “Keys” 创建入口 | 不是 Account Holder | 让 Account Holder 代建 API Key |
| -25294 无法导入证书 | 证书与私钥不匹配或钥匙串未解锁 | 重新用本机 CSR 申请,或导入 .p12 |
| Archive contains critical validation errors | 包内存在未签名的 .dylib/.jnilib/.node | 自检脚本找出 → afterSign.js 补签或排除 |
| does not have a ticket stapled | 未装订公证票据 | xcrun stapler staple 到 .app/.dmg |
| 长时间 In Progress | 队列繁忙或网络慢 | 稍等几分钟或重试 |
| 首次运行仍弹提示 | 其实公证未通过/未装订 | 走三板斧验证,确保 Accepted + staple |
🧭 十二、复盘总结
本文从问题场景出发,按证书 → 签名 → 公证 → 装订 → 验证的顺序,解决 Electron .dmg 在 macOS 上的合法性问题,并提供:
- ✅ 详细的证书获取步骤:从生成 CSR、申请证书到导入钥匙串的完整流程;
- ✅ API Key 获取指南:创建、下载和管理 App Store Connect API Key 的详细说明;
- ✅ 三种 Electron 项目配置方式:硬编码、环境变量、CI/CD 集成的完整示例;
- ✅ GitHub Actions CI/CD 配置:包括证书导入、秘密管理的生产级配置;
- ✅ 安全管理最佳实践:.gitignore 配置、敏感信息管理、权限最小化等;
- ✅ 可直接落地的
electron-builder.yml+entitlements.mac.plist模板(均为 mock 示例); - ✅ 公证前自检脚本:在提交前发现未签名的原生库;
- ✅ 自动补签的
afterSign.js(适用于第三方原生依赖); - ✅ 多个常见坑点与权限问题(Keys 不可见、证书导入失败等)的定性与修复。
按本文执行后,你的
.dmg将具备签名 + 公证 + 票据装订的完整链路,用户安装时不再被系统拦截。无论是本地开发还是 CI/CD 自动化,都能找到对应的解决方案。祝分发顺利!