做 Windows、macOS、Linux 桌面应用时怎么选框架、怎么守住渲染层和系统之间的安全边界、怎么签名公证、打包、自动更新、上报崩溃,以及怎么在真机上验收。
写明支持矩阵:每个目标的操作系统和版本、CPU 架构、窗口系统(Linux)、安装格式和分发渠道;CI 构建矩阵与之一致
怎么检查:仓库里有一份结构化的支持矩阵文件;CI 脚本比对矩阵和构建任务,缺项时报错
为什么、来源
为什么:「支持 Linux」「支持 Windows」不是一个布尔值;不列清楚,就没法决定构建目标、测试机器和安装格式。
- Linux 要写到发行版和版本、glibc 和 WebKitGTK 的基线、X11 还是 Wayland。
- 系统已停止支持的版本(如已结束支持的 Windows 版本)若仍要支持,写明理由和风险。
框架选型写进决策记录,列出至少一个备选、取舍理由和推翻条件;不在规范层规定「一律用某个框架」
怎么检查:评审决策记录是否包含候选、取舍、推翻条件;有条件时附同场景小样的安装包大小、内存和启动时间对比
为什么、来源
为什么:包体积、内存这类宣传数字受是否打包运行时影响很大,不在自己的场景里比一次,选型就是凭印象。
使用 Qt 开源版时,按 LGPLv3 履行义务:动态链接、允许用户替换并重新链接 Qt 库、随附许可证文本和源码获取方式,且不使用仅 GPL 授权的模块;做不到就购买商业许可
怎么检查:发布前由负责人核对:链接方式、所用 Qt 模块的许可证、随包的许可证声明和源码获取说明;拿不准的交法务确认
为什么、来源
为什么:Qt 采用商业和开源双许可,部分模块只有 GPL;不满足 LGPL 要求时不能分发,锁死设备或静态链接也可能违约。
- 静态链接可能使应用本身受 LGPL 约束;Qt 官方建议动态链接。
- LGPLv3 要求提供足够的安装信息,让用户能运行重新链接后的程序,封闭设备基本做不到。
- 部分应用商店条款可能与 LGPL 冲突,上架前核对。
正式发布构建不使用 alpha、beta、nightly 版的框架、打包工具和更新器
怎么检查:CI 检查锁文件里这些依赖的版本号不含预发布标记
为什么、来源
为什么:预发布版的接口、权限默认值和签名格式还会变,出了安全问题也不一定有补丁。
需要试用新特性时,放在单独的预发布渠道,不混进正式渠道。
Electron 应用使用官方仍在支持的主版本(最新的三个稳定主版本之一),并跟随安全补丁升级
怎么检查:CI 或定时任务比对项目的 Electron 主版本和官方发布时间表,超出支持范围时报错
参考值:「最新三个稳定主版本」「每 8 周一个主版本」来自 Electron 官方发布时间表。
为什么、来源
为什么:Electron 每 8 周发布一个主版本,只给最新三个稳定主版本修安全问题;落后的版本带着已公开的 Chromium 漏洞。
界面只通过命名的、带类型和参数校验、逐方法授权的窄命令调用系统能力;不向界面暴露通用的执行命令、读写任意文件、结束任意进程的接口
怎么检查:列出桥接层暴露的全部命令并评审;用 lint 或架构检查禁止在桥接层出现通用执行、任意路径、按进程号操作的接口;为每个命令写拒绝用例
为什么、来源
为什么:渲染层一旦被跨站脚本或恶意内容控制,桥接层暴露什么,攻击者就拿到什么。
- 命令按业务语义设计(如「启动任务 123」),由宿主层查到对应资源再操作,不接受界面传来的命令行、绝对路径或进程号。
- 每个命令有请求和返回的结构定义、错误码;改变状态的命令记录调用者、时间和结果,不记录明文密钥。
Electron 的所有窗口必须保持 contextIsolation、sandbox、webSecurity 打开,nodeIntegration 关闭
怎么检查:lint 或静态扫描检查所有 BrowserWindow / webPreferences 配置;启动时断言这些选项的实际值
为什么、来源
为什么:这几个开关是 Electron 官方安全清单的基础项;关掉任何一个,渲染层的脚本漏洞就可能变成本机代码执行。
Electron 的 preload 只用 contextBridge 暴露命名的具体方法,不把 ipcRenderer、require 或整个模块暴露给页面
怎么检查:静态扫描 preload 文件,发现暴露 ipcRenderer、require 或透传任意通道名的写法时报错
为什么、来源
为什么:整个 ipcRenderer 暴露出去,页面就能向任意通道发消息,白名单形同虚设,而且以后只会越开越大。
Electron 主进程的每个 IPC 处理函数都校验消息发送方和入参结构
怎么检查:处理函数集中注册,单元测试覆盖「来自非预期来源」和「参数不合法」两类反例
为什么、来源
为什么:任何能加载进来的页面或子框架都可能发 IPC 消息,不校验发送方就等于对所有来源开放。
处理函数在应用启动时注册一次,不要写在创建窗口的函数里,否则开第二个窗口会重复注册。
拦截不受控的页面跳转和新窗口;打开外部链接只放行白名单协议,不把用户输入直接交给 shell.openExternal 一类接口
怎么检查:测试覆盖跳转到非预期地址、window.open、file: 协议和自定义协议链接等情况;静态扫描 openExternal 的调用点
为什么、来源
为什么:能被诱导跳到外部页面的窗口会把原生能力交给外部页面;openExternal 传入恶意地址可能直接执行本机程序。
生产版设置经过评审的内容安全策略(CSP),不加载远程脚本、不使用 eval
怎么检查:静态检查页面和打包配置中的 CSP;启动后的冒烟测试读取实际生效的 CSP
为什么、来源
为什么:CSP 是阻止注入脚本执行的最后一道防线;远程脚本一旦被篡改,就以应用的身份运行。
不可信的远程网页、第三方插件内容、HTML 预览不放进有原生桥的窗口;需要展示时用不带桥的隔离视图或交给系统浏览器
怎么检查:评审时列出每个窗口和视图加载的内容来源,核对加载远程内容的视图没有 preload 或 capability
为什么、来源
为什么:远程内容一旦出现脚本注入,就能直接调用本机能力。
Tauri 的 capability 必须绑定到具体窗口,只授予该窗口用到的权限;不授予通用的命令执行权限
怎么检查:CI 解析 capabilities 目录,检查每个 capability 指定了窗口,且不含禁用清单里的权限(如 shell 插件的通用执行权限);权限文件变化时要求安全评审
为什么、来源
为什么:同一窗口命中的多个 capability 权限会合并,一个宽泛的配置就会让所有窗口获得高危能力。
需要调用外部程序时,用 sidecar 或 shell 插件的作用域限定程序名和参数,不开放任意命令。
改动 preload、主进程窗口配置、Tauri capability、深链处理、凭据存储、更新器和签名脚本时,必须经安全负责人评审
怎么检查:把这些路径写进 CODEOWNERS,分支保护要求负责人批准
为什么、来源
为什么:这些文件决定安全边界,功能测试全绿也看不出权限被放宽了。
界面和本机后台之间优先用框架自带的进程间通信;确需开本机 HTTP 端口时,遵守 32 的本机服务防护规则
怎么检查:评审时检查是否新开了监听端口;开了的按 RUN-038、RUN-039、RUN-040 逐项核对
为什么、来源
为什么:多开一个端口就多一个能被网页和同机其他程序访问的入口。
令牌和密码只存系统凭据库(macOS 钥匙串、Windows 凭据管理器或 DPAPI、Linux Secret Service),不放 localStorage、明文配置文件或日志
怎么检查:密钥扫描覆盖仓库和构建日志;测试登录后检查应用数据目录和浏览器存储里没有明文令牌
为什么、来源
为什么:明文文件和浏览器存储会被备份、同步、被其他程序读走,泄露后也难以察觉。
系统凭据库挡不住同一用户权限下运行的恶意程序,仍要配合短期令牌和服务端撤销。
Linux 上没有可用的安全凭据后端时(如 Electron safeStorage 退化为 basic_text),不保存敏感令牌,提示用户配置凭据服务或改为每次登录
怎么检查:启动时读取实际使用的存储后端并在退化时走专门分支;在 GNOME、KDE、无钥匙环三种环境下测试
为什么、来源
为什么:basic_text 的「加密」用的是固定口令,实际上等于明文。
用户数据写入系统规定的用户数据目录,不写安装目录;有本地数据库的,每次升级用旧版本数据测试迁移
怎么检查:测试在只读安装目录、普通用户权限下运行;发布前用上一版的数据样本跑升级
为什么、来源
为什么:安装目录在升级和卸载时会被替换,普通用户往往也没有写权限;迁移失败会让用户打不开应用。
深链只认固定的协议名,参数按结构严格校验,只映射到预先定义的动作;不执行链接里的命令,不直接打开链接里的文件路径
怎么检查:对深链解析函数做模糊测试;测试应用已在运行时深链能交给已有实例处理(单实例)
为什么、来源
为什么:深链能被任何网页或文档触发,是来自外部的不可信输入。
托盘、通知、自启动、文件关联用框架官方接口或插件实现;自启动默认关闭,用户和企业策略都能关掉
怎么检查:依赖评审确认使用官方插件;测试设置页开关后读取系统里的实际状态
为什么、来源
为什么:这些功能的系统差异和生命周期边界很多,自己造容易出错;暗中自启动会引起用户和企业 IT 的反感。
明确「关闭窗口」是退出还是缩到托盘,并在第一次缩到托盘时提示用户。
要求离线可用时,首次启动不依赖开发服务器、CDN、远程字体或远程配置,所有关键资源随包打包
怎么检查:在断网的干净虚拟机里安装并首启,同时记录 DNS 和网络请求;静态扫描打包产物里的外部地址
为什么、来源
为什么:内网或断网环境下,一个远程字体或脚本加载失败就可能白屏。
使用系统 WebView 的应用(Tauri、Wails)面向不能联网的 Windows 环境时,安装包内置 WebView2 离线安装包或固定版本运行时,或由 IT 预装并验证
怎么检查:检查打包配置中 WebView2 的安装方式;在断网的 Windows 虚拟机上首装首启
参考值:Tauri 官方给出的量级:离线安装包约增加 127 MB,固定版本运行时约增加 180 MB。
为什么、来源
为什么:默认的下载引导方式和内嵌引导程序在安装时都要联网下载运行时,断网就装不上。
要求在内网构建时,提前准备 npm、Cargo、Go 等依赖镜像、框架二进制下载镜像和离线安装物料,并在无外网环境里完整构建一次
怎么检查:在断网的构建机上从干净状态完成依赖恢复、构建、签名和打包
为什么、来源
为什么:Electron 等框架在安装时会额外下载二进制,没有镜像就构建不起来。
在 Mac App Store 之外分发的 macOS 应用和安装包,必须用 Developer ID 证书签名、开启 Hardened Runtime、用 notarytool 公证并附上公证票据
怎么检查:发布任务里运行 codesign --verify --deep --strict、spctl -a -vv、xcrun stapler validate,任一失败即阻断发布
为什么、来源
为什么:用 Developer ID 分发的软件必须公证,否则 Gatekeeper 会拦截;附票后离线机器也能验证。
- .app 用 Developer ID Application 证书,.pkg 用 Developer ID Installer 证书;签名带安全时间戳。
- 应用包里的子程序、动态库、sidecar 都要签名。
- Apple 自 2023-11-01 起不再接受 altool 提交公证。
- 公证要联网,完全断网的客户可以接收在联网流水线里签好、公证好、附好票的成品。
- Electron 应用在 macOS 上要自动更新,必须先签名。
面向公众分发的 Windows 安装包和可执行文件,必须用公共信任的证书做 Authenticode 签名并加时间戳;企业内部用私有证书时,要有 IT 已下发根证书的证明
怎么检查:发布任务用 signtool verify 或 Get-AuthenticodeSignature 检查每个可执行文件和安装包的签名状态与时间戳
为什么、来源
为什么:未签名的程序会被 SmartScreen 拦截并显示警告;没有时间戳的签名在证书过期后就失效了。
扩展验证(EV)证书已不再自动免除 SmartScreen 提示,新签名的应用仍需积累信誉。
签名证书和私钥放在硬件安全模块或云签名服务里,只在受保护的发布任务中使用,不放进仓库、普通文件或开发者电脑
怎么检查:密钥扫描覆盖仓库;评审发布流水线,确认签名步骤只在受保护分支和环境里运行
为什么、来源
为什么:签名私钥泄露后,攻击者可以用你的身份发布恶意软件。
Linux 的 deb、rpm 通过签名的软件仓库分发;AppImage 等独立文件同时提供校验和与签名,并说明验证方法
怎么检查:发布任务检查仓库元数据签名和校验和文件存在;安装测试从仓库安装
为什么、来源
为什么:没有签名的包无法证明没被替换;AppImage 本身不会自动验证签名。
面向企业统一部署的安装包,必须支持静默安装、升级和卸载,退出码可靠
怎么检查:在虚拟机里用静默参数安装、升级、卸载,检查退出码和检测规则
为什么、来源
为什么:Intune 这类部署工具要求 Win32 应用能静默安装,需要用户点击的安装器无法批量部署。
写清是按用户安装还是按机器安装、安装路径、是否需要管理员权限和重启。
Linux 版在支持的最老发行版上构建,或在每个目标发行版上分别构建;不把一个 AppImage 当作支持所有 Linux
怎么检查:CI 构建环境与支持矩阵里最老的发行版一致;每个目标发行版真机启动一次
为什么、来源
为什么:在新系统上构建的程序会依赖新版 glibc,在老系统上直接起不来;AppImage 仍依赖宿主的 glibc、FUSE、WebKitGTK。
Flatpak 应用通过 portal 访问文件和系统能力,不常开主机文件系统等宽泛权限
怎么检查:CI 检查 Flatpak 清单里的 finish-args,出现宽泛文件系统权限时要求写明理由
为什么、来源
为什么:宽泛权限破坏沙箱隔离,也可能不符合分发平台的审核要求。
每个安装渠道只指定一个更新负责方(企业 IT、系统包管理器或应用内更新器);企业部署的 MSI、MSIX、PKG、deb、rpm 默认由 IT 管控,关闭应用内更新
怎么检查:发布配置里为每个渠道写明更新负责方;评审时核对企业包已关闭应用内更新
为什么、来源
为什么:应用自更新和 IT 部署同时改同一份安装,会互相覆盖、版本错乱,还绕过了企业的变更流程。
自动更新用成熟的更新器,不自己写;更新包必须签名,安装前验证签名、版本和目标架构
怎么检查:评审更新器选型;测试被篡改的更新包、错误架构的包会被拒绝
为什么、来源
为什么:签名校验、断电恢复、文件占用、回退都很难做对;更新通道一旦能被篡改,就能把恶意程序推到所有用户机器上。
- Tauri 的更新插件强制校验更新签名,不能关闭。
- electron-updater 在 macOS 和 Windows 上都校验代码签名,Windows 上会做 Authenticode 校验。
- 更新包签名和系统代码签名是两层信任,不能互相代替。
发布前测试更新的异常情况:下载中断、更新包被篡改、降级、从上一个正式版升级后数据可用
怎么检查:发布测试清单包含这些场景并记录结果
为什么、来源
为什么:更新失败会让用户卡在打不开的版本上,而且只有在真实升级时才会暴露。
接入崩溃上报,覆盖原生崩溃和主进程、渲染进程的未捕获异常;发布时保存符号文件和 source map,以便还原堆栈
怎么检查:每次发布的产物清单里有符号文件;测试版人为触发一次崩溃,确认能看到还原后的堆栈
为什么、来源
为什么:桌面应用运行环境千差万别,没有崩溃上报,只能靠用户描述猜问题。
未经用户或企业配置同意,不向公网上传崩溃报告和使用数据;上传前脱敏,上报地址可改为客户自建的服务或关闭
怎么检查:在未同意状态下抓包确认没有对外请求;对脱敏函数做单元测试;检查上报地址是配置项
为什么、来源
为什么:崩溃转储可能含内存片段、本机路径、令牌和业务数据,未经同意外传有隐私和合规风险。
每个声明支持的「系统 × CPU 架构 × 安装格式」,发布前都在真机或对应的虚拟机上完成安装、首启、升级和卸载;不以交叉编译成功或 x64 仿真代替
怎么检查:发布记录里按支持矩阵逐项附安装日志或截图
为什么、来源
为什么:CPU 架构、系统证书、WebView 版本、原生模块的问题只有在目标机器上才会暴露。
Windows ARM64 上,Electron 的原生模块要按 ARM64 重新编译。
测试报告分开写网页功能、桥接层、系统集成、安装发布四层结果;网页 E2E 通过不得报告为桌面集成通过
怎么检查:测试报告模板固定四层栏目,评审时核对每层的证据
为什么、来源
为什么:浏览器里的测试测不到托盘、安装器、凭据库、系统通知和签名,混在一起报告会掩盖没测的部分。
在每个目标系统上验证深色模式、系统缩放、输入法、多显示器、睡眠唤醒后恢复和断网;键盘与读屏按 09 的要求人工测试
怎么检查:真机检查清单逐项记录;Windows 用 Accessibility Insights 辅助检查
为什么、来源
为什么:这些是桌面应用特有、自动化测试很难覆盖的高发故障点。
国产操作系统和 CPU(统信、麒麟、龙芯、飞腾、鲲鹏等)只在客户同款系统和 CPU 上真机验证通过后才宣称支持,不从「支持 Linux」推出
怎么检查:支持矩阵里国产平台逐项标注验证状态和验证机型
为什么、来源
为什么:内核、CPU 指令集、WebKitGTK 供给和桌面环境都可能不同,没测过的承诺到交付时才暴露。
没有符合条件的规则。
用 Web 技术做界面的桌面框架
Tauri(系统 WebView + Rust 宿主) · Tauri 为 MIT 或 Apache-2.0 双许可;Electron 为 MIT
备选:Electron(要求各平台 Chromium 渲染一致、依赖 Node 原生模块或成熟的 Electron 生态);Wails(业务逻辑以 Go 为主;有不可信的子框架或远程内容时先核实其安全公告)
不推荐:把 WKWebView、WebKitGTK 当 Chrome 用;只因为「Electron 包大」就一律不用
不用 Web 技术的跨平台桌面界面
沿用团队已有的技术栈:已有 Flutter 用 Flutter 桌面,已有 Kotlin 用 Compose Multiplatform,已有 C++ 或需要嵌入式、工业场景用 Qt · Flutter 核心为 BSD-3-Clause;Compose Multiplatform 为 Apache-2.0;Qt 为商业许可或 LGPLv3/GPL 双许可
不推荐:为了复用 React 代码选 Flutter 或 Compose;不核实许可证就用 Qt 开源版做闭源产品
Electron 打包工具
Electron Forge(Electron 官方维护)
备选:electron-builder(需要 electron-updater,或要一次产出 NSIS、AppImage、deb、rpm 等多种格式)
不推荐:两套打包工具同时用
Electron 自动更新
与打包工具配套:electron-builder 配 electron-updater;Forge 配 Electron 内建的 autoUpdater
不推荐:自己写更新器;在 Linux 上指望内建 autoUpdater
Tauri 自动更新
Tauri 官方更新插件(强制校验更新签名)
企业设备的安装与更新
Windows 用 MSI 或 MSIX + Intune 等管理工具;macOS 用签名公证的 PKG + MDM;Linux 用签名的 deb/rpm 私有仓库
不推荐:需要用户点击的安装器;应用内更新和 IT 部署同时开启
个人用户下载安装
Windows 用 NSIS;macOS 用签名公证的 DMG;Linux 用 AppImage 或发行版包
备选:Flatpak(希望在沙箱里运行,并通过 portal 访问系统能力);Mac App Store、Microsoft Store(需要商店分发;规则待补调研)
不推荐:未签名的 ZIP 当安装包
Windows 代码签名
云端托管的签名服务(如 Microsoft Artifact Signing,原名 Trusted Signing),符合资格时使用
备选:商业 CA 的组织验证(OV)证书,私钥放在硬件安全模块或云密钥服务(所在地区或主体类型不符合托管签名服务的资格);企业私有证书(只在企业内部分发,且 IT 已向所有终端下发根证书)
不推荐:私钥文件放在仓库或开发者电脑上;指望 EV 证书免除 SmartScreen 提示
本地凭据存储
系统凭据库(macOS 钥匙串、Windows 凭据管理器或 DPAPI、Linux Secret Service);Electron 用 safeStorage
不推荐:localStorage、明文 JSON 配置;自己用硬编码口令加密
崩溃上报
本地脱敏日志和转储 + Sentry 协议的 SDK(Electron 用 sentry-electron),上报地址可配置 · GlitchTip 为 MIT;Sentry 自托管仓库为 FSL-1.1-Apache-2.0,不是宽松许可证,集成或对外提供服务前按 27 评审
备选:GlitchTip 自托管(私有化部署,需要轻量的错误收集服务;原生转储的支持程度先验证);Sentry 自托管(需要完整功能且接受其许可证条款)
不推荐:默认上传到公有云服务
桌面 E2E 测试工具
Electron 用 Playwright 的 Electron 支持;Tauri 用 WebdriverIO(Tauri 官方文档介绍的 WebDriver 方案) · Playwright 为 Apache-2.0
备选:Appium 或系统 UI 自动化接口(Windows UI Automation 等)(要测安装器、托盘、系统对话框等框架外的界面)
不推荐:只跑浏览器里的网页测试
✕ 错误做法:Electron 里开 nodeIntegration 或关掉 sandbox 图省事
✓ 应该:保持安全开关全开,通过 preload 暴露命名方法
✕ 错误做法:把整个 ipcRenderer 暴露给页面
✓ 应该:contextBridge 只暴露具体方法,主进程逐个校验
✕ 错误做法:Tauri 里全局开放命令执行权限
✓ 应该:用 sidecar 或作用域限定程序名和参数
✕ 错误做法:界面把进程号传给宿主让它结束进程
✓ 应该:按业务资源编号由宿主查找并在授权后操作
✕ 错误做法:深链 app://run?cmd=... 直接执行
✓ 应该:只解析预定义动作和编号,严格校验参数
✕ 错误做法:把更新器的签名当成系统代码签名
✓ 应该:系统签名公证和更新包签名两层都做
✕ 错误做法:.app 签了名就当公证完成
✓ 应该:签名后用 notarytool 公证并附票
✕ 错误做法:把 Tauri 的内嵌引导程序当作离线 WebView2
✓ 应该:用离线安装包或由 IT 预装
✕ 错误做法:页面运行时从 CDN 加载字体和样式
✓ 应该:关键静态资源全部随包打包
✕ 错误做法:应用内更新和 Intune 同时更新同一份安装
✓ 应该:每个渠道只指定一个更新负责方
✕ 错误做法:崩溃转储默认全部上传到公有云
✓ 应该:本地保存脱敏数据,经同意或按企业配置上报到指定地址
✕ 错误做法:用 Qt 开源版静态链接后闭源分发
✓ 应该:动态链接并履行 LGPL 义务,或购买商业许可
✕ 错误做法:同一台 CI 机器编译成功就算各平台都支持
✓ 应该:按支持矩阵逐项真机安装验证