Electron

第十一章:签名、公证与生产发布

第十一章:签名、公证与生产发布

目录


为什么必须签名

未签名的应用在现代操作系统上会被拦截,用户体验极差:

未签名应用的用户体验:

  macOS (Gatekeeper):
  ┌─────────────────────────────────────┐
  │  ⚠️ "MyApp" 无法打开,因为          │
  │     无法验证开发者。                 │
  │                                     │
  │            [移到废纸篓]  [取消]      │
  │                                     │
  │  用户必须:系统偏好设置 → 安全性     │
  │  → "仍要打开" 才能运行               │
  └─────────────────────────────────────┘

  Windows (SmartScreen):
  ┌─────────────────────────────────────┐
  │  ⚠️ Windows 已保护你的电脑          │
  │                                     │
  │  SmartScreen 阻止了一个未识别的      │
  │  应用启动。运行此应用可能有风险。     │
  │                                     │
  │  [不运行]     [更多信息 → 仍要运行]  │
  └─────────────────────────────────────┘

  签名后:
  ✅ macOS: 双击直接打开,无任何警告
  ✅ Windows: SmartScreen 不拦截(EV 证书)或仅提示发布者名称
签名的作用:

  1. 身份验证 — 证明应用来自可信开发者
  2. 完整性保障 — 确保代码未被篡改
  3. 平台要求 — macOS/Windows 强制要求
  4. 自动更新 — 更新包必须签名才能安装

  签名流程概览:

  源代码 → 构建 → 签名 → 公证(macOS) → 分发
                    │       │
                    │       └─ Apple 服务器扫描恶意软件
                    └─ 用开发者证书对二进制签名

macOS 签名与公证

前置条件

macOS 签名需要:

  1. Apple Developer Program 会员($99/年)
     https://developer.apple.com/programs/

  2. 两个证书(在 Keychain Access 中管理):
     ┌──────────────────────────────────────────────┐
     │  Developer ID Application  — 签名应用本体    │
     │  Developer ID Installer    — 签名 .pkg 安装包│
     └──────────────────────────────────────────────┘

  3. Apple ID 的 App-Specific Password
     https://appleid.apple.com → 安全 → App 专用密码

  4. Team ID(在 Apple Developer 后台查看)

@electron/osx-sign + @electron/notarize

npm install -D @electron/osx-sign @electron/notarize
// 手动签名(了解原理用)
const { signAsync } = require('@electron/osx-sign');
const { notarize } = require('@electron/notarize');

// 第一步:签名
await signAsync({
  app: 'dist/MyApp.app',
  identity: 'Developer ID Application: Your Name (TEAM_ID)',
  optionsForFile: (filePath) => ({
    // Hardened Runtime 是公证的前提条件
    hardenedRuntime: true,
    entitlements: 'entitlements.plist',
    entitlementsInherit: 'entitlements.plist',
  }),
});

// 第二步:公证
await notarize({
  appPath: 'dist/MyApp.app',
  appleId: process.env.APPLE_ID,
  appleIdPassword: process.env.APPLE_APP_PASSWORD,
  teamId: process.env.APPLE_TEAM_ID,
});

Hardened Runtime 与 Entitlements

macOS 公证要求启用 Hardened Runtime,某些 Electron 功能需要额外授权:

<!-- entitlements.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <!-- 允许 JIT(V8 引擎需要) -->
  <key>com.apple.security.cs.allow-jit</key>
  <true/>
  <!-- 允许加载未签名的内存页(某些原生模块需要) -->
  <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
  <true/>
  <!-- 允许 DYLD 环境变量(调试用,生产可移除) -->
  <key>com.apple.security.cs.disable-library-validation</key>
  <true/>
</dict>
</plist>

Electron Forge 配置方式

如果使用 Electron Forge,签名和公证可以在配置中一体化完成:

// forge.config.js
module.exports = {
  packagerConfig: {
    osxSign: {
      identity: 'Developer ID Application: Your Name (TEAM_ID)',
      optionsForFile: () => ({
        hardenedRuntime: true,
        entitlements: 'entitlements.plist',
        entitlementsInherit: 'entitlements.plist',
      }),
    },
    osxNotarize: {
      appleId: process.env.APPLE_ID,
      appleIdPassword: process.env.APPLE_APP_PASSWORD,
      teamId: process.env.APPLE_TEAM_ID,
    },
  },
  // makers 配置...
};

electron-builder 配置方式

# electron-builder.yml
mac:
  category: public.app-category.developer-tools
  hardenedRuntime: true
  entitlements: build/entitlements.plist
  entitlementsInherit: build/entitlements.plist

afterSign: scripts/notarize.js  # 签名后自动公证
// scripts/notarize.js — afterSign 钩子
const { notarize } = require('@electron/notarize');

exports.default = async function notarizing(context) {
  const { electronPlatformName, appOutDir } = context;
  if (electronPlatformName !== 'darwin') return;

  const appName = context.packager.appInfo.productFilename;

  await notarize({
    appPath: `${appOutDir}/${appName}.app`,
    appleId: process.env.APPLE_ID,
    appleIdPassword: process.env.APPLE_APP_PASSWORD,
    teamId: process.env.APPLE_TEAM_ID,
  });
};
macOS 签名+公证完整流程:

  构建 .app


  代码签名 (codesign)
  ├─ 对每个二进制文件签名
  ├─ 嵌入 Hardened Runtime 标记
  └─ 应用 entitlements


  提交公证 (notarytool)
  ├─ 上传到 Apple 服务器
  ├─ Apple 自动扫描恶意软件
  └─ 返回公证结果(通常 2-5 分钟)


  Staple 公证票据
  ├─ 将公证凭据附加到 .app
  └─ 离线也能验证签名


  打包为 .dmg 或 .pkg

Windows 签名

EV 代码签名证书

自 2023 年起,微软要求新发布者使用 EV(Extended Validation)代码签名证书才能立即获得 SmartScreen 信任:

Windows 签名证书类型:

  ┌─────────────┬────────────────────┬──────────────────────┐
  │ 类型        │ 标准代码签名       │ EV 代码签名          │
  ├─────────────┼────────────────────┼──────────────────────┤
  │ 价格        │ ~$200/年           │ ~$400/年             │
  │ SmartScreen │ 需要积累信誉       │ 立即信任             │
  │ 存储要求    │ 可以软件存储       │ 必须硬件存储(USB/HSM)│
  │ CI 友好     │ 好                 │ 需要云签名方案       │
  │ 推荐场景    │ 个人/小团队        │ 正式商业发布         │
  └─────────────┴────────────────────┴──────────────────────┘

  主流证书提供商:
  • DigiCert  — 企业首选,提供 KeyLocker 云签名
  • Sectigo   — 性价比高
  • GlobalSign — 企业级方案

@electron/windows-sign

npm install -D @electron/windows-sign
const { sign } = require('@electron/windows-sign');

await sign({
  appDirectory: 'dist/win-unpacked',
  // 使用 signtool 签名
  signToolPath: 'C:\\Program Files (x86)\\Windows Kits\\10\\bin\\signtool.exe',
  certificateFile: process.env.WIN_CERT_FILE,
  certificatePassword: process.env.WIN_CERT_PASSWORD,
  timestampServer: 'http://timestamp.digicert.com',
});

云签名方案(CI 环境推荐)

EV 证书存储在硬件令牌中,CI 环境无法插 USB。解决方案是使用云签名服务:

云签名架构:

  CI 服务器 (GitHub Actions)

       │  API 调用(证书密钥在云端)

  ┌──────────────────────┐
  │  云签名服务           │
  │  DigiCert KeyLocker  │
  │  Azure Trusted Sign  │
  │  AWS CloudHSM        │
  ├──────────────────────┤
  │  HSM (硬件安全模块)   │
  │  私钥永不离开硬件     │
  └──────────────────────┘


  签名后的二进制文件
# electron-builder.yml — Windows 签名配置
win:
  target:
    - nsis
    - zip
  signingHashAlgorithms:
    - sha256
  # 使用 DigiCert KeyLocker
  sign: scripts/windows-sign.js
// scripts/windows-sign.js
exports.default = async function(configuration) {
  // DigiCert KeyLocker 签名示例
  const { execSync } = require('child_process');
  execSync(`smctl sign --keypair-alias ${process.env.SM_KEYPAIR_ALIAS} \
    --certificate ${process.env.SM_CLIENT_CERT_FILE} \
    --input "${configuration.path}"`, { stdio: 'inherit' });
};

Linux 分发注意事项

Linux 不要求代码签名,但分发有自身的复杂性:

Linux 分发格式:

  ┌──────────┬──────────────────────────────┬──────────────────┐
  │ 格式     │ 说明                         │ 适用发行版       │
  ├──────────┼──────────────────────────────┼──────────────────┤
  │ AppImage │ 单文件,下载即用             │ 通用             │
  │ .deb     │ Debian 包管理器              │ Ubuntu/Debian    │
  │ .rpm     │ Red Hat 包管理器             │ Fedora/RHEL      │
  │ snap     │ Canonical 沙箱分发           │ Ubuntu           │
  │ flatpak  │ 跨发行版沙箱分发             │ 通用             │
  └──────────┴──────────────────────────────┴──────────────────┘

  推荐策略:
  • AppImage — 作为通用格式,必出
  • .deb     — Ubuntu 用户量大,建议出
  • snap     — 如果需要自动更新功能
# electron-builder.yml — Linux 配置
linux:
  target:
    - AppImage
    - deb
    - rpm
  category: Development
  icon: build/icons/  # 需要提供多个尺寸的 PNG

# AppImage 注意事项
appImage:
  artifactName: '${name}-${version}.AppImage'

# deb 注意事项
deb:
  depends:
    - libgtk-3-0
    - libnotify4
    - libnss3
    - libxss1

自动更新的发布流程

electron-updater 元数据文件

electron-builder 配合 electron-updater 使用时,会自动生成更新元数据文件:

构建产物目录:

  dist/
  ├── MyApp-1.2.0.dmg              # macOS 安装包
  ├── MyApp-1.2.0-mac.zip          # macOS 自动更新用
  ├── latest-mac.yml               # macOS 更新元数据 ←
  ├── MyApp-Setup-1.2.0.exe        # Windows 安装包
  ├── latest.yml                   # Windows 更新元数据 ←
  ├── MyApp-1.2.0.AppImage         # Linux
  └── latest-linux.yml             # Linux 更新元数据 ←
# latest.yml 示例(自动生成,不要手动编辑)
version: 1.2.0
files:
  - url: MyApp-Setup-1.2.0.exe
    sha512: abc123...
    size: 85000000
path: MyApp-Setup-1.2.0.exe
sha512: abc123...
releaseDate: '2025-01-15T10:30:00.000Z'

更新服务器选择

┌────────────────────┬───────────────────────────────────────────┐
│ 方案               │ 说明                                     │
├────────────────────┼───────────────────────────────────────────┤
│ GitHub Releases    │ 免费,适合开源项目。electron-updater 原生支持│
│ S3 / R2 / OSS     │ 适合商业项目,成本低,速度可控             │
│ 自建更新服务器      │ 完全自控,可实现灰度发布等高级功能         │
│ Hazel / Nuts       │ 开源的更新服务器,部署在 Vercel/Heroku     │
└────────────────────┴───────────────────────────────────────────┘
// main.js — 配置自动更新
const { autoUpdater } = require('electron-updater');

// GitHub Releases
autoUpdater.setFeedURL({
  provider: 'github',
  owner: 'your-org',
  repo: 'your-app',
});

// 或 S3
autoUpdater.setFeedURL({
  provider: 's3',
  bucket: 'my-app-updates',
  region: 'us-east-1',
});

// 或自建服务器
autoUpdater.setFeedURL({
  provider: 'generic',
  url: 'https://updates.myapp.com/releases',
});

灰度发布(Staged Rollout)

灰度发布可以先让一部分用户更新,确认无问题后再全量推送:

灰度发布策略:

  新版本 v1.3.0 发布


  阶段1: 5% 用户更新  ──→  监控崩溃率、反馈
       │                    正常 ✅

  阶段2: 20% 用户更新 ──→  继续监控
       │                    正常 ✅

  阶段3: 50% 用户更新 ──→  观察一天
       │                    正常 ✅

  全量推送: 100%

  如果任何阶段发现问题 → 暂停推送,修复后重新发布
# latest.yml — 灰度发布配置
version: 1.3.0
files:
  - url: MyApp-Setup-1.3.0.exe
    sha512: abc123...
    size: 85000000
stagingPercentage: 20  # 只有 20% 的用户能看到此更新
// electron-updater 会自动根据 stagingPercentage 决定是否提示更新
// 判断逻辑:对用户标识取 hash,hash % 100 < stagingPercentage 则更新

崩溃上报

crashReporter 模块

Electron 内置了崩溃上报能力,底层基于 Crashpad(Chromium 使用的崩溃收集器):

// main.js — 配置崩溃上报
const { crashReporter } = require('electron');

crashReporter.start({
  productName: 'MyApp',
  companyName: 'MyCompany',
  submitURL: 'https://sentry.io/api/PROJECT_ID/minidump/?sentry_key=KEY',
  uploadToServer: true,
  extra: {
    appVersion: app.getVersion(),
    platform: process.platform,
  },
});

Sentry 集成

Sentry 是最常用的崩溃上报服务,对 Electron 有完善的支持:

npm install @sentry/electron
// main.js
const Sentry = require('@sentry/electron/main');
Sentry.init({
  dsn: 'https://xxx@sentry.io/yyy',
  release: `my-app@${app.getVersion()}`,
});

// renderer (preload 暴露后在渲染进程初始化)
// renderer.js
const Sentry = require('@sentry/electron/renderer');
Sentry.init({});
崩溃上报流程:

  应用崩溃


  Crashpad 生成 minidump 文件


  ┌────────────────┐     上传      ┌──────────────────┐
  │  本地崩溃目录   │ ──────────►  │  Sentry / 自建    │
  │  .dmp 文件      │              │  崩溃收集服务器   │
  └────────────────┘              └──────────────────┘


                                  解析 stack trace
                                  聚合同类崩溃
                                  通知开发者

CI/CD 完整流水线

GitHub Actions 全平台构建、签名、发布

# .github/workflows/release.yml
name: Build & Release

on:
  push:
    tags:
      - 'v*'  # 推送 tag 时触发(如 v1.2.0)

jobs:
  build:
    strategy:
      matrix:
        include:
          - os: macos-latest
            platform: mac
          - os: windows-latest
            platform: win
          - os: ubuntu-latest
            platform: linux
    runs-on: ${{ matrix.os }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci

      # ── macOS: 导入证书 ──
      - name: 导入 macOS 签名证书
        if: matrix.platform == 'mac'
        env:
          CERT_BASE64: ${{ secrets.MAC_CERT_BASE64 }}
          CERT_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }}
        run: |
          echo "$CERT_BASE64" | base64 --decode > cert.p12
          security create-keychain -p "" build.keychain
          security import cert.p12 -k build.keychain -P "$CERT_PASSWORD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple: -s -k "" build.keychain
          security default-keychain -s build.keychain
          rm cert.p12

      # ── 构建 + 签名 + 公证 + 发布(一条命令搞定) ──
      - name: 构建并发布
        env:
          # macOS 签名 & 公证
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          CSC_LINK: ${{ secrets.MAC_CERT_BASE64 }}
          CSC_KEY_PASSWORD: ${{ secrets.MAC_CERT_PASSWORD }}
          # Windows 签名
          WIN_CSC_LINK: ${{ secrets.WIN_CERT_BASE64 }}
          WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERT_PASSWORD }}
          # GitHub Token(发布到 Releases)
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: npx electron-builder --publish always

      # ── 上传构建产物 ──
      - uses: actions/upload-artifact@v4
        with:
          name: release-${{ matrix.platform }}
          path: |
            dist/*.dmg
            dist/*.zip
            dist/*.exe
            dist/*.AppImage
            dist/*.deb
            dist/*.yml
CI/CD 流水线全景:

  开发者 push tag v1.2.0


  ┌──────────────────────────────────────────────────────────┐
  │  GitHub Actions(并行三平台)                              │
  │                                                          │
  │  macOS Runner        Windows Runner     Linux Runner     │
  │  ┌──────────┐       ┌──────────┐       ┌──────────┐    │
  │  │ npm ci   │       │ npm ci   │       │ npm ci   │    │
  │  │ build    │       │ build    │       │ build    │    │
  │  │ sign     │       │ sign     │       │ package  │    │
  │  │ notarize │       │          │       │          │    │
  │  │ publish  │       │ publish  │       │ publish  │    │
  │  └──────────┘       └──────────┘       └──────────┘    │
  │       │                  │                  │           │
  │       └──────────────────┼──────────────────┘           │
  │                          ▼                               │
  │              GitHub Releases v1.2.0                      │
  │              ├── MyApp-1.2.0.dmg                        │
  │              ├── MyApp-1.2.0-mac.zip                    │
  │              ├── MyApp-Setup-1.2.0.exe                  │
  │              ├── MyApp-1.2.0.AppImage                   │
  │              ├── latest-mac.yml                         │
  │              ├── latest.yml                             │
  │              └── latest-linux.yml                       │
  └──────────────────────────────────────────────────────────┘


  用户应用内收到自动更新通知

常见问题

1. macOS 公证失败

常见错误及解决方案:

  "The signature does not include a secure timestamp"
  → 签名时使用 --timestamp 标志(@electron/osx-sign 默认已处理)

  "The executable does not have the hardened runtime enabled"
  → 确保 hardenedRuntime: true

  "The signature is invalid"
  → 检查 entitlements.plist 是否正确
  → 确保所有 .node 原生模块也被签名

  公证超时(> 10 分钟)
  → Apple 服务器繁忙,稍后重试
  → 确认网络连接正常

2. Windows SmartScreen 仍然报警

原因:标准代码签名证书需要积累信誉
  • 新证书签名的应用初期仍会被 SmartScreen 标记
  • 需要足够多的用户下载运行后才能建立信任

解决方案:
  • 使用 EV 代码签名证书(立即信任)
  • 提交到 Microsoft 进行手动审查
  • 确保签名包含时间戳(防止证书过期后失效)

3. CI 环境中如何安全管理证书

原则:证书私钥绝不能明文出现在代码仓库中

  推荐做法:
  ┌──────────────────────────────────────────────────┐
  │  1. 将 .p12 证书文件 base64 编码                  │
  │     cat cert.p12 | base64 > cert_base64.txt      │
  │                                                   │
  │  2. 存入 GitHub Secrets                           │
  │     Settings → Secrets → New repository secret    │
  │     MAC_CERT_BASE64 = <粘贴 base64 内容>          │
  │     MAC_CERT_PASSWORD = <证书密码>                 │
  │                                                   │
  │  3. CI 中还原                                     │
  │     echo "$CERT_BASE64" | base64 --decode > cert  │
  └──────────────────────────────────────────────────┘

4. electron-builder vs Electron Forge 的签名配置差异

electron-builder:
  • 通过 CSC_LINK / CSC_KEY_PASSWORD 环境变量自动签名
  • afterSign 钩子处理公证
  • 配置在 electron-builder.yml

Electron Forge:
  • 在 forge.config.js 的 packagerConfig 中配置
  • osxSign + osxNotarize 字段
  • 更贴近 Electron 生态

实践建议

┌──────────────────────────────────────────────────────────────┐
│           签名与发布最佳实践                                  │
│                                                              │
│  1. 从第一天就签名                                           │
│     不要等到发布时才处理签名问题                              │
│     在 CI 中尽早配置签名流程                                 │
│                                                              │
│  2. 证书管理                                                 │
│     • 证书存储在 CI Secrets 中,不入代码库                    │
│     • 记录证书到期日,提前续期                                │
│     • EV 证书选择支持云签名的提供商                           │
│                                                              │
│  3. macOS 公证不可跳过                                       │
│     macOS 10.15+ 强制要求公证                                │
│     没有公证的应用在 macOS 上几乎无法使用                     │
│                                                              │
│  4. 自动更新必须签名                                         │
│     electron-updater 会验证更新包的签名                       │
│     未签名的更新包会被拒绝安装                                │
│                                                              │
│  5. 灰度发布降低风险                                         │
│     新版本先推给 5-10% 用户                                   │
│     监控崩溃率稳定后再全量推送                                │
│                                                              │
│  6. 崩溃上报必须有                                           │
│     至少集成 Sentry 或类似服务                                │
│     崩溃日志是修复线上问题的唯一途径                          │
│                                                              │
│  7. 版本号规范                                                │
│     遵循 SemVer(major.minor.patch)                          │
│     electron-updater 依赖版本号判断更新                       │
│                                                              │
│  8. 发布清单                                                 │
│     □ 更新版本号(package.json)                              │
│     □ 更新 CHANGELOG                                         │
│     □ 创建 git tag(v1.2.0)                                 │
│     □ 推送 tag 触发 CI                                       │
│     □ CI 自动构建 → 签名 → 公证 → 发布                       │
│     □ 验证 GitHub Releases 产物                               │
│     □ 手动下载安装验证                                        │
│     □ 验证自动更新推送正常                                    │
└──────────────────────────────────────────────────────────────┘

下一章:我们将学习 Electron 的跨平台差异与原生模块开发 →