简介这是一份面向iOS开发者与测试人员的V2签名网站系统全套源码基于PHP后端实现IPA在线签名、UDID自动获取、签名码生成及多开APP安装等核心流程可帮助用户在免装Xcode等本地工具的前提下完成应用签名与分发。开发者只需部署在Nginx、PHP7.4和MySQL5.6环境中即可快速搭建自有签名平台。压缩包共2000个文件包含1795个JSON配置及数据文件、149个Markdown说明文档、51个HTML管理页面及SQL初始化脚本类型覆盖前端界面、后端逻辑、数据库结构和部署文档总量约76.66MB目录结构清晰便于二次开发。目前已有289人学习下载适合需要搭建在线签名服务或研究iOS签名机制的初中级开发者参考。开源版本包含完整业务闭环从设备信息采集到签名码校验均有对应实现可直接运行并扩展为生产级系统。1. 在 Linux 上跑通 IPA 在线签名先绕开 codesign 依赖一句话IPA 重签名最耗时间的环节不是签名命令本身而是收包 → 选证书 → 换描述文件 → 改 Bundle ID → 签名 → 生成下载页 → 让测试设备在 Safari 里直接装这一整条链路。在线签名网站系统解决的就是这条链路的自动化问题你上传一个原始 IPA选好 p12 证书和描述文件系统在后台完成重签名再产出一个能用 itms-services 协议直接安装的链接。团队多证书、多包、多设备时手工流程完全不可控这也是 V2 签名网站系统这类开源项目流行的原因。它的核心并不神秘只是把签名工具、证书池、任务队列和分发服务四样东西拼了起来。读完之后你会发现即使手上没有某个特定版本的开源源码自己也能从零搭出同样能力的最小系统。2. 在线签名的原理与 V2 系统的模块拆分2.1 一次重签名动的是哪三样东西IPA 本质是一个 zip 压缩包解压后是Payload/xxx.app目录。一次重签名不是在包外面加一道锁而是把包内已有的签名信息整体替换成新证书生成的签名信息。完整重签名会改动三处第一处是embedded.mobileprovision描述文件。它决定这个包能装到哪些设备、能声明哪些 entitlements比如 App Group、推送权限、数据保护。描述文件里同时绑定了 App ID 和证书指纹App 安装时系统会校验这些关系。第二处是Info.plist里的CFBundleIdentifier。Bundle ID 必须与描述文件里注册的 App ID 精确一致否则签名完成后安装阶段直接被拒绝和证书有没有过期的报错感观完全不同。第三处是所有可执行文件的_CodeSignature目录。包括主程序、扩展 appex、Watch 附属等漏掉任何一个都会导致安装失败。签名校验时系统会把每个可执行文件的哈希与签名里锁定的哈希比对再验证证书链是否可信因此只换描述文件但不重签在逻辑上是不成立的。这里有个新手常见误区以为 p12 证书有效就一定能装。实际上签名内容与描述文件互为校验证书有效但描述文件里没有设备 UDID依然提示无法安装。这也是在线签名系统必须在每个任务里做完整重签名而不是做增量修改的原因。2.2 V2 签名系统在业务上拆成了四个模块一套能对外稳定服务的 IPA 签名网站功能上要覆盖证书、包、任务、安装四个维度市面上能跑起来的开源版本页面各不相同但底层模块基本一致证书池管理模块负责上传 p12 证书和描述文件记录 Bundle ID、证书归属、有效期签名任务生成时从池子里匹配证书对。应用包管理模块负责接收原始 IPA解析版本号、图标、原始 Bundle ID签名成功后回写输出路径和下载地址。签名任务队列模块把上传动作与签名动作解耦因为一个大包签名耗时几秒到几十秒同步等待会拖垮页面请求。分发安装模块则为每个产物生成 manifest plist通过 itms-services 协议触发设备下载。四个模块不是平级的任务队列是核心枢纽证书池是前置条件分发链路是最终出口。我一般建议先实现队列和证书池再往上套 UI因为全部业务逻辑都集中在队列里Web 层只是传递参数。2.3 服务端重签名工具为什么选 zsign 而不是 codesign如果你在 macOS 上用官方 codesign依赖 Xcode 工具链和钥匙串授权这在无头服务器上非常难处理。codesign 调的是 macOS 的 Security 框架Linux 上没有对应实现所以在线签名系统不会把 codesign 作为服务端标准组件。V2 类系统在服务端重签名时最常选用的 IPA 签名工具是 zsign一个纯 C 实现的跨平台工具输入 p12、密码、描述文件和原始 IPA直接输出重签名后的 IPA。它内部会自动导出使用到的 entitlements、处理多架构二进制、一并签名扩展和附属不依赖 macOS。与 ldid 对比ldid 更适合检查与修改签名结构对正式企业证书的发布流程支持不如 zsign 直接输出文件也不一定带完整的签名块在多台设备内测分发这个场景里zsign 一步出 IPA 的路径最短。2.4 在 Linux 服务器上编译 zsignzsign 编译依赖很少gcc/g、OpenSSL 头文件、zlib 就够了。先把它拉到自己的 Git 仓库不要直接在主仓上改然后在干净的 Ubuntu 22.04 上编译sudo apt update sudo apt install -y build-essential libssl-dev zlib1g-dev make mkdir -p /opt/ipa-sign/bin cp zsign /opt/ipa-sign/bin/ /opt/ipa-sign/bin/zsign 21 | head -20逻辑说明build-essential提供 gcc/g 与 makelibssl-dev提供 RSA 与证书解析能力zlib 用于解压和重组 IPA 的 zip 结构。编译产物是单文件 zsign放到固定目录后后续 worker 脚本统一用绝对路径调用避免 PATH 不一致导致排查困难。参数说明直接运行 zsign 会打印 usage最常用的组合如下表参数组合作用-k cert.p12 -p 123456 -m profile.mobileprovision签名并自动从描述文件推断 entitlements-k cert.p12 -p 123456 -b com.example.newid签名并同时改写 Bundle ID-k cert.p12 -p 123456 -o out.ipa in.ipa指定输入输出文件路径注意-b改写 Bundle ID 时工具会同步修改 Info.plist但你必须在签名前确认描述文件里的 App ID 与新 ID 匹配。编译通过后不要急着接 Web先用命令行手工签一次拿到正确产物再去自动化。3. 从空库到第一个在线签名接口表结构与任务队列3.1 三张表把签名业务存清楚数据库表结构建议按证书、包、任务三组记录来建这样拆的好处是任务历史与包信息解耦签名失败后能反复查原因而不会污染原始包数据CREATE TABLE certs ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64) NOT NULL, bundle_id VARCHAR(255) NOT NULL DEFAULT , p12_file VARCHAR(255) NOT NULL, p12_pass VARCHAR(128) NOT NULL DEFAULT , provision_file VARCHAR(255) NOT NULL DEFAULT , expired_at DATETIME DEFAULT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE apps ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(128) NOT NULL, bundle_id VARCHAR(255) NOT NULL, ipa_file VARCHAR(255) NOT NULL, cert_id INT UNSIGNED DEFAULT 0, new_bundle_id VARCHAR(255) NOT NULL DEFAULT , status TINYINT NOT NULL DEFAULT 0, output_ipa VARCHAR(255) DEFAULT , manifest_url VARCHAR(255) DEFAULT , created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sign_tasks ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, app_id INT UNSIGNED NOT NULL, action VARCHAR(16) NOT NULL DEFAULT sign, status TINYINT NOT NULL DEFAULT 0, message TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, finished_at DATETIME DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明certs 表存的是 p12 文件与描述文件的相对路径不是二进制本身p12 的明文密码在开源版本里各有做法常见的是加密后落库或从环境变量注入但字段必须存在后续 zsign 才能以非交互方式执行。apps 表用 status 标记包状态0 待签名、1 签名中、2 成功、3 失败。sign_tasks 表把每次签名动作独立成一行失败时可保留完整报错。参数说明apps 表同时保存bundle_id与new_bundle_id支持同一个原始包签出多个渠道 ID这是多证书场景的隐藏需求。签名成功后manifest_url字段保存待会儿要生成的 plist 外链前端下载按钮直接读这个字段即可。3.2 上传接口只做一件事把任务推进队列Web 端一定不要同步调用 zsign。上传接口的责任是落盘、写库、推入任务队列马上返回 appId。以 Node/Express 为例代码可以这样写const multer require(multer); const fs require(fs); const upload multer({ dest: /opt/ipa-sign/files/uploads, fileFilter(req, file, cb) { cb(null, file.originalname.toLowerCase().endsWith(.ipa)); } }); app.post(/api/ipa, upload.single(ipa), async (req, res) { const meta JSON.parse(req.body.meta || {}); const ipaPath req.file.path; const appId await saveApp({ name: meta.appName || req.file.originalname, bundle_id: meta.bundleId || , ipa_file: ipaPath, cert_id: meta.certId || 0, status: 0 }); await pushQueue(sign: appId); res.json({ code: 0, appId: appId }); });逻辑说明fileFilter 只做最小校验真正的 IPA 合法性交给签名任务去验证因为 zip 结构解析、Mach-O 读取都必须等文件落盘后才能做。pushQueue可以接 Redis 的 LPUSH也可以直接往 sign_tasks 表插一行由 worker 用状态扫描代替独立队列小规模部署时后者更省事。参数说明meta里传递的certId对应 certs 表主键如果做自动化 API这里可以不传 certId由 worker 根据原始 Bundle ID 自动匹配描述文件形成证书池的规则匹配逻辑。res 返回的 appId 会作为后续查询签名状态的入参。3.3 签名 worker 的循环脚本worker 是所有组件里应该最先跑通的一环。先做成 bash 脚本手动传参验证再接轮询#!/bin/bash # /opt/ipa-sign/scripts/sign_task.sh ZSIGN/opt/ipa-sign/bin/zsign CERT$1 PASS$2 PROV$3 IN_IPA$4 OUT_IPA$5 NEW_BID$6 if [ -n $NEW_BID ]; then $ZSIGN -k $CERT -p $PASS -m $PROV -b $NEW_BID -o $OUT_IPA $IN_IPA else $ZSIGN -k $CERT -p $PASS -m $PROV -o $OUT_IPA $IN_IPA fi echo exit$?逻辑说明脚本把 zsign 的复杂度封装成六个固定位置参数worker 从 sign_tasks 表取到任务后把对应字段填进来调用即可。$?是上一条命令的退出码0 表示签名成功非 0 表示失败外层 worker 依据它决定将任务状态置为 2 还是 3并把 stderr 写入 message 字段。参数说明NEW_BID为空时zsign 保留原包 Bundle ID非空时才追加-b参数。生产环境里证书和描述文件可能不止一套建议在任务入队前就把匹配好的证书路径与描述文件路径写入任务行worker 保持无状态不要自己在脚本里选证书这样并发时不会互相踩到。提示worker 脚本里所有临时目录必须按任务 ID 隔离两个并发任务共用同一个 tmp 目录会导致 zsign 解压冲突。常见做法是mkdir -p /tmp/sign_$TASK_ID签名完成后删除该目录只保留 output 产物。4. 部署到服务器目录布局、Nginx 与 itms-services 分发链路4.1 服务端目录布局把上传、输出、证书、脚本分开权限边界清晰后面用 Nginx 做静态映射时才不会把 p12 露到公网。推荐的布局是/opt/ipa-sign/ ├── app/ # Web 后端工程 ├── bin/ # zsign 二进制 ├── certs/ # p12 证书仅 worker 可读 ├── files/ │ ├── uploads/ # 原始 IPA │ ├── output/ # 签名后的 IPA │ └── profiles/ # mobileprovision ├── scripts/ │ ├── sign_task.sh │ └── make_manifest.sh └── logs/ # worker 日志这个结构的关键在于 certs 和 profiles 不会被 Nginx 的根目录覆盖到。输出目录用 Nginx alias 暴露给下载链接而上传目录通常不对外直接访问因为原始包也含有签名信息虽然没有隐私泄漏风险但从管理上没必要开放。4.2 Nginx 配置下载页与分发口必须走 HTTPSitms-services 协议要求 manifest plist 和 IPA 文件都通过可信 HTTPS 提供否则设备端在很多 iOS 版本上会直接忽略安装请求。配置上拆两个 server 块更清晰server { listen 443 ssl; server_name ipa.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; # 管理后台反代 location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } # 签名产物下载不要用 root 拼接路径 location /ipa/ { alias /opt/ipa-sign/files/output/; add_header Content-Disposition attachment; filenameapp.ipa; } # manifest plist 走独立目录 location /manifest/ { alias /opt/ipa-sign/files/manifests/; default_type application/xml; } }逻辑说明管理后台走反代到 Web 服务IPA 下载走 alias 映射到 output 目录manifest 目录单独暴露出来。plist 的 Content-Type 有些浏览器识别有问题写成application/xml最稳。参数说明alias与root的区别是 alias 会整体替换掉匹配前缀/ipa/a.ipa实际对应/opt/ipa-sign/files/output/a.ipa而 root 会把完整 URL 路径拼在后面容易写出 404。HTTPS 证书建议用 certbot 申请可信证书自签证书在设备安装时通常会被拦。4.3 第一次端到端签名与下载验证部署完成后不要先点页面直接在服务器上跑一遍完整链路确认最小闭环cd /opt/ipa-sign ./scripts/sign_task.sh \ ./certs/prod.p12 123456 \ ./files/profiles/dev.mobileprovision \ ./files/uploads/demo.ipa \ ./files/output/demo_signed.ipa \ com.example.demo命令含义传入证书、密码、描述文件、输入 IPA、输出 IPA 和新 Bundle ID。成功后检查输出文件大小是否与原始包接近然后用 unzip 抽查签名结构unzip -l output/demo_signed.ipa | grep _CodeSignature unzip -p output/demo_signed.ipa Payload/*.app/embedded.mobileprovision | head -c 200说明第一条命令列出_CodeSignature目录确认签名块存在第二条直接输出描述文件开头字节快速确认它已经被替换成你传入的那份。如果这两处都正常基本可以打包下载验证安装。4.4 生成可安装的 itms-services 链接下载安装不是直接给 .ipa 的 URLiOS 需要先读取一份 manifest plist。生成脚本可以这样写#!/bin/bash # make_manifest.sh ipa_url bundle_id version title img_url out_file IPA_URL$1 BID$2 VER$3 TITLE$4 IMG$5 OUT$6 cat $OUT EOF ?xml version1.0 encodingUTF-8? !DOCTYPE plist PUBLIC -//Apple//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd plist version1.0 dict keyitems/key array dict keymetadata/key dict keybundle-identifier/key string$BID/string keybundle-version/key string$VER/string keykind/key stringsoftware/string keytitle/key string$TITLE/string /dict keyassets/key array dict keykind/key stringsoftware-package/string keyurl/key string$IPA_URL/string /dict dict keykind/key stringdisplay-image/string keyurl/key string$IMG/string /dict /array /dict /array /dict /plist EOF echo $OUT逻辑说明manifest plist 里bundle-identifier必须与签名后的 Bundle ID 一致software-package的 url 必须是 HTTPS 的 .ipa 地址。系统自带图标的情况下display-image 可以省略但有图标时安装界面会好看很多。参数说明脚本里所有变量都用双引号包裹避免标题含空格造成 plist 拼坏。生成后的安装链接格式是itms-services://?actiondownload-manifesturlhttps://ipa.example.com/manifest/demo.plist把这个链接生成二维码测试设备用 Safari 扫码就会弹出安装确认。注意微信等 App 内打开不会触发必须要用 Safari。5. 实测中的四个坑与分钟级验证手段5.1 安装失败先从描述文件和 UDID 排设备端提示无法安装的瞬间先不要怀疑服务器配置按这张表逐项排查报错现象最可能原因验证方式Safari 提示无法连接到 itms-servicesHTTPS 证书不受信换可信证书后重试弹出安装但进度条很快失败设备 UDID 不在描述文件中用描述文件工具重新生成提示App 已损坏或无法验证部分二进制漏签zsign 重签时加 verbose 输出安装后闪退entitlements 与描述文件不一致对比新旧描述文件的授权项UDID 不匹配是重签名分发里出现频率最高的问题。开发者证书与描述文件是两回事证书决定谁能签描述文件里的设备列表决定能装到哪。测试设备换过或者新增都必须重新生成包含新 UDID 的描述文件再执行一次签名。提示如果签名时改了 Bundle ID务必确认描述文件里的 App ID 也是同一个新值否则安装阶段报错比过期证书更难察觉日志里通常只会给一个笼统的无法安装。5.2 检查描述文件与证书是否匹配的快速手段把证书和描述文件解出来比对一分钟内就能确认一对证书能否用于当前任务# 看描述文件里证书的指纹 security cms -D -i dev.mobileprovision 2/dev/null | grep -A 20 DeveloperCertificates # 看 p12 里的证书指纹 openssl pkcs12 -in prod.p12 -nokeys -passin pass:123456 | openssl x509 -noout -subject -fingerprint逻辑说明描述文件里的 DeveloperCertificates 字段保存的是证书 DER 内容p12 里是证书链对比两者的 SHA-1 指纹是否一致即可。如果描述文件更新过而 p12 没换指纹对不上签名依然能成功但设备端在信任阶段会失败。5.3 让 worker 日志直接可检索把任务失败时的关键信息输出成一行结构化 JSON比粘一大段 stderr 好排错得多。worker 里这样追加日志echo {\task\:$TASK_ID,\app\:$APP_ID,\exit\:$?,\msg\:\$(tail -c 200 $LOG_FILE | tr \n )\} logs/sign.log说明单行 JSON 方便用 grep 按 app 或 task 过滤不需要启动任何日志平台。msg 里取 stderr 最后 200 字节能覆盖 zsign 报错的核心原因又不会被刷屏。最后再给一个实用技巧把 p12 密码从脚本参数改成环境变量注入比如 worker 从.env读取避免密码散落在 shell 历史或日志文件里。这套最小系统能跑通的前提就是签名命令可复现、失败原因可查、产物可验证把这三个基础打牢再往上面加用户体系、证书池自动匹配和安装统计都由你随意扩展。本文还有配套的精品资源点击获取