
简介iOS V2签名网站系统源码是一套面向iOS开发者、测试人员及越狱玩家的开源免授权签名平台用于搭建私有签名环境整套系统无需授权即可使用。其核心功能包括通过上传设备证书文件获取UDID、选择或上传待签名软件包一键完成签名、生成并管理签名码以及支持在一台设备上多开安装不同版本或配置的应用可显著降低应用测试与分发的门槛。压缩包共2000个文件以1793个json配置文件为核心辅以149个md说明文档、53个html管理页面及少量txt、sql、js文件整体体积约95.9MBjson多用于环境配置与数据存储md覆盖部署与二次开发说明html则是后台管理界面目录结构清晰便于部署、学习与二次开发。目前已有521人查看学习资源内附带视频教程可帮助用户从环境搭建到实际签名操作快速上手。对于希望深入了解iOS签名原理、自建分发系统或参与开源改进的开发者这份源码是一个低门槛的参考实现不过使用时须遵守Apple相关规定并关注签名策略更新以保证系统持续可用。1. 开源的 iOS V2 签名网站系统省掉的不只是证书钱一台 Mac 被反复用来处理新设备证书申请、描述文件导入和 Xcode 打包UDID 录入靠同事截图配置一天改三遍。一套开源的 iOS V2 签名网站系统正是为这个场景做的在一个 Web 控制台里管理 App、证书和描述文件把用户打开页面、安装描述文件、回传 UDID、重签 IPA 到生成下载链接这一整条链路串起来。V2 在这里指系统迭代到第二个版本源码开源免授权意味着不用向原开发者购买 License可以自己部署、改模块、去品牌水印。它适合 iOS 开发团队、内测服务商、以及想自建分发工具链的运维。下面从签名链路的最底层讲起一直讲到部署参数、常见报错和更适合工程化的进阶接法目标是让你不用看视频教程也能把这套系统跑通。2. 签名网站不是替代码打签名证书、描述文件与 RSA 签名验签2.1 签名与描述文件的层级关系iOS 正式分发体系里一个 IPA 能否被某台设备安装取决于两层东西。第一层是代码签名App 的二进制、动态库、图片资源都会通过 macOS 的codesign工具用私钥做签名公钥放在证书里设备启动时用证书链校验完整性。第二层是 Provisioning Profile也就是描述文件。它本身是一个由 Apple 用 CMS 格式签名过的 plist里面写清楚了允许使用的证书、App ID 对应的 Entitlements、可安装的设备 UDID 列表。一个签名网站系统要正常工作必须同时满足三个条件代码签名的证书出现在描述文件的DeveloperCertificates数组中设备的 UDID 出现在ProvisionedDevices中描述文件里的application-identifier与 App 的 Bundle Identifier 一致。签名网站真正管理的是描述文件、证书私钥和 UDID 列表而codesign动作仍要交给 macOS 上的命令行服务来完成。所以你会看到很多签名系统的后端是 PHP 或 Node但签名 worker 一定跑在 macOS 上通过 SSH、HTTP API 或消息队列被调动。这也是新手最容易理解错的地方源码里并不需要内置一个“苹果签名算法”它管的是证书和描述文件的配对关系。V2 系统的价值不在于能代替 App Store而在于把过去要开 Xcode 才能完成的 Ad Hoc 和按设备授权流程变成了网页上的一键操作。证书类型设备限制安装方式典型用途iOS Development单设备调试Xcode 直装开发联调Ad Hoc最多 100 台网页链接 描述文件内测分发Enterprise不限设备数网页链接 描述文件企业内部大规模分发App Store不允许直接安装App Store对外发布V2 签名网站系统更多面向 Ad Hoc 和 Enterprise 场景。Ad Hoc 上限 100 台所以 UDID 注册和去重是核心功能企业证书不限制设备数但证书本身由 Apple 严格管控使用时要格外注意账号合规性。2.2 用一段 Python 读出描述文件里的关键信息部署签名系统之前先学会把描述文件拆开看。描述文件本质上是一段 CMS 签名数据直接用plutil打不开需要用security cms -D解开。#!/usr/bin/env python3 import subprocess import plistlib from pathlib import Path def parse_profile(path: Path) - dict: # 用 security cms -D 解开 CMS 签名得到原始 plist out subprocess.check_output( [security, cms, -D, -i, str(path)], stderrsubprocess.DEVNULL ) profile plistlib.loads(out) print(Name:, profile.get(Name)) print(AppID:, profile.get(Entitlements, {}).get(application-identifier)) print(Devices:, profile.get(ProvisionedDevices, [])) print(Certs:, [c for c in profile.get(DeveloperCertificates, [])]) return profile if __name__ __main__: parse_profile(Path(embedded.mobileprovision))这个脚本的价值在于当系统后台报“设备未授权”时你不需要登录 Apple Developer 后台反复确认直接把它跑一遍就知道描述文件里有没有这台设备。plistlib.loads需要传入已经解包后的 plist 数据不能直接传原始 mobileprovision 文件所以代码里先用security cms -D做解包。-i指定输入文件把结果打印到标准输出。常见做法的补充如果描述文件里ProvisionedDevices缺失就代表它是一份 App Store 或 Enterprise 类型描述文件后者的设备校验在服务端和 Ad Hoc 的校验位置不同。排错前先分类。2.3 在 macOS 上验证 RSA 签名是否有效描述文件本身是 Apple 用 RSA 私钥做的签名收到描述文件的系统侧会拿 Apple 根证书公钥验签。对网站系统而言更常见的是验证手里的 IPA 是否被正确签名以及证书链是否完整。# 查看当前钥匙串里可用的签名证书 security find-identity -v -p codesigning # 验证 .app 的签名状态 codesign -dv --verbose4 YourApp.app # 深度验证签名和资源完整性 codesign --verify --deep --strict --verbose2 YourApp.appsecurity find-identity输出的CSSMERR_TP_CERT_EXPIRED或-67050都说明证书过期。codesign -dv打印的AuthorityiPhone Developer: ...代表当前签名的证书链Signature expired字样说明签名时间戳问题不是证书问题。V2 签名系统的前端页面不需要做 RSA 验签但后端在接受到上传的 IPA 后应该先执行一次codesign --verify避免把一个已经被破坏的包推到用户面前。3. 从源码到系统V2 签名网站的模块划分与数据流3.1 开源仓库里通常有哪些模块从 GitHub 或 Gitee 搜“iOS 签名 网站 源码”常见到的 V2 版本大体由这么几块构成用户和权限模块、证书仓库、描述文件生成模块、应用上传与版本管理、UDID 回调接收、签名任务队列、下载页生成、以及给前端用的 API。后端有纯 PHP 的也有 PHP Redis 队列或 Node MySQL 的。拿到源码后先看目录结构重点找sign、worker、udid、manifest、profile这几个关键词。一个合格的 V2 系统会把签名 worker 和 Web 服务分开而不是让 PHP 进程直接去操作 Keychain。如果源码里强行用exec(codesign ...)要注意执行权限和并发冲突这是后面调优的根因。模块职责关键实现点证书仓库保存 p12、cer、私钥私钥加密存储上传时校验密码描述文件管理按 AppID 和证书组合生成 profile调用 Apple 开发后台或本地模板App 管理上传 IPA记录 Bundle ID、版本号自动解析 Info.plistUDID 回调接收设备安装描述文件后的跳转解析 Safari 回调参数签名任务调用 macOS 下的 codesign / resign单队列执行防止 Keychain 冲突下载页生成 itms-services 链接和二维码必须走 HTTPS选型建议团队只有一条开发证书时直接找单体 PHP 实现就够了如果要做成多人协作平台一定要选带队列和任务状态的版本否则两个并发签名任务会互相挤占钥匙串。3.2 用户点击下载后发生了什么从用户角度整个过程不过三步打开网页允许安装描述文件再点下载 IPA。但从系统角度看数据流是这样的。Web 页面生成一个描述文件下载链接指向https://sign.example.com/profile/xxx.mobileprovision。用户用 Safari 打开该链接系统弹窗提示“此网站正尝试下载一个配置描述文件”。允许安装后iOS 会自动回调描述文件里的 URL一般形如https://sign.example.com/udid?udidxxxxidentifiercom.example.app。后端接收回调检查 UDID 是否已经在证书允许的设备列表里不在则更新描述文件。描述文件准备好后后端生成一个 manifest plist里面指向真正的 IPA 下载地址。用户点击“安装 App”系统通过itms-services://?actiondownload-manifesturl...拉起安装流程。很多系统卡在第三步因为描述文件里配置的回调 URL 写成了http或者回调地址里带了换行。Safari 对描述文件回调的要求是标准 HTTPS URL且不能有空格和换行符。部署前可以先手动把描述文件下载下来用第二节的 Python 脚本确认url字段。3.3 用 PHP 生成 itms-services 清单的关键代码manifest 是一个 XML plist它告诉 iOS 去哪里下载 IPA 和图标。下面的函数是签名网站里最常见的一段代码几乎每个开源 V2 系统都能找到对应版本。?php function buildManifest( string $ipaUrl, string $bundleId, string $appVersion, string $title, string $iconUrl ): string { $xml XML ?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 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__ICON_URL__/string /dict /array keymetadata/key dict keybundle-identifier/key string__BUNDLE_ID__/string keybundle-version/key string__VERSION__/string keykind/key stringsoftware/string keytitle/key string__TITLE__/string /dict /dict /array /dict /plist XML; return str_replace( [__IPA_URL__, __ICON_URL__, __BUNDLE_ID__, __VERSION__, __TITLE__], [ htmlspecialchars($ipaUrl, ENT_QUOTES | ENT_XML1, UTF-8), htmlspecialchars($iconUrl, ENT_QUOTES | ENT_XML1, UTF-8), htmlspecialchars($bundleId, ENT_QUOTES | ENT_XML1, UTF-8), htmlspecialchars($appVersion, ENT_QUOTES | ENT_XML1, UTF-8), htmlspecialchars($title, ENT_QUOTES | ENT_XML1, UTF-8), ], $xml ); }代码里的__IPA_URL__必须是一个 HTTPS URL且不能带重定向display-image不是必填但建议只放一张 512x512 的 PNG尺寸太大会增加安装前等待。返回的 XML 字符串在输出时必须设置响应头Content-Type: application/xml否则 iOS 可能不识别。常见的跳转方式是生成一个下载入口然后把用户带到itms-services://?actiondownload-manifesturl这里放manifest地址。由于 App Store 不允许普通网页直接唤起安装系统通常会先让用户确认“是否打开此页面”然后一次点击安装。4. 从源码部署到真机装包V2 签名系统最小闭环4.1 部署后端与初始化数据库拿到源码后的第一步不是急着上传 IPA而是把后端和数据库跑起来。假设你选择的是一个兼容 PHP 现代框架的版本本地克隆后的初始化命令大概是这样的。git clone 你fork的开源仓库地址 ios-sign-server cd ios-sign-server cp .env.example .env vim .env composer install php artisan key:generate php artisan migrate --seed.env里至少需要确认四项数据库连接DB_HOST、DB_DATABASE、DB_USERNAME、DB_PASSWORD队列驱动QUEUE_CONNECTION生产环境建议改成redis再配置后台默认管理员账号。如果你下到的是传统 PHP 源码没有 Composer那就直接创建 MySQL 数据库把根目录里的xxx.sql导入然后改config/database.php里的连接参数。提示不要把签名 worker 放在容器里跑。codesign需要直接访问 macOS 的钥匙串容器里除非直通/Library/Keychains和security命令否则签名会报找不到证书。常见做法是 Web 部署在一台 Linux 或 Mac 上签名 worker 单独放到一台 Mac mini 上通过内部 API 触发。4.2 Mac 签名 worker 的最小脚本下面这段脚本可以作为自定义签名 worker 的起点。它的作用是把一个 IPA 解包、替换描述文件、重新签名再打回 IPA。#!/usr/bin/env bash set -euo pipefail IPA$1 PROFILE$2 CERT_NAME$3 OUT$4 TMP$(mktemp -d) trap rm -rf $TMP EXIT unzip -q $IPA -d $TMP APP$(find $TMP -maxdepth 2 -name *.app | head -n 1) cp $PROFILE $APP/embedded.mobileprovision /usr/bin/codesign --force --sign $CERT_NAME \ --timestampnone \ --entitlements $TMP/entitlements.plist \ $APP # 动态库和 Framework 需要单独签 find $APP/Frameworks -name *.dylib -o -name *.framework 2/dev/null | while read -r f; do /usr/bin/codesign --force --sign $CERT_NAME --timestampnone $f done cd $TMP zip -qry $OUT Payload脚本里的--timer其实是--timestampnone关闭时间戳可以避免签名服务传递时间服务不可用时失败。--entitlements只对主 App 生效Framework子项不要带 entitlements。zip -qry Payload保证最终目录结构是Payload/App.app如果打包时路径多了一层安装会直接失败。在真实项目里通常不会手动维护这个脚本而是用 Fastlane 的sigh或match来生成描述文件再用gym打包。V2 系统的价值恰恰是把这些命令行步骤封装成网页后台操作但你仍然需要在 macOS 上预先把证书导入登录钥匙串并把证书的 Common Name 和CERT_NAME对齐。4.3 HTTPS 和 iOS 开发者模式是跑通的必要条件itms-services 协议要求 manifest 和 IPA 都必须放在合法 HTTPS 地址下自签名证书在 iOS 上会被直接拒绝。用 Nginx 作为 Web 入口配置好反代后申请证书常用命令如下。sudo certbot --nginx -d sign.example.com # 自动续期 echo 0 3 * * * certbot renew --deploy-hook nginx -s reload | sudo crontab -Nginx 里还需要单独处理两个静态目录manifest 和 IPA。核心配置如下。location /manifests/ { alias /var/www/ios-sign/storage/manifests/; default_type application/xml; } location /apps/ { alias /var/www/ios-sign/storage/apps/; add_header Content-Disposition attachment; limit_rate 4m; }limit_rate 4m是给下载限速避免一个测试包占满出口带宽Content-Disposition attachment让浏览器直接下载 IPA而不是尝试预览。配置完成后用curl -I https://sign.example.com/manifests/demo.plist检查响应头确认Content-Type是application/xml。设备这里的准备也容易被忽略iOS 16 之后的系统在正式安装企业级描述文件前需要在“设置 - 隐私与安全性 - 开发者模式”里打开开发者模式。不开的话设备重启后不会响应描述文件安装点击下载只会一直转圈。这也是很多管理员排错到最后才发现的问题。5. 参数调优与排错描述文件申请失败、UDID 收不到、安装卡住5.1 一张常见的错误定位表把签名系统从搭建到上线过程中最高频的几个问题整理成表先对照现象确认方向再动手看日志。现象可能原因处理方式描述文件申请失败提示get xcodetoken err或hsc200Apple ID 登录 token 失效或二次验证不通过使用 App Store Connect API Key 替代账号密码登录用户点击安装描述文件后一直转圈描述文件回调 URL 是 HTTP 或包含换行重新生成描述文件URL 必须为 HTTPSUDID 回调地址收到请求但参数为空描述文件里的url被截断检查服务器访问日志对比 Safari 实际跳转地址Safari 提示无法下载 Appmanifest 或 IPA 的 Content-Type 错误检查 Nginx 的default_type和扩展名签名时报-19061私钥不在登录钥匙串中用security import重新导入 p12安装后打开闪退描述文件与 AppID 不匹配用第 2.2 节的脚本核对application-identifier这里的get xcodetoken err是很多自动签名工具共有的报错本质是 Apple 服务端没有给你想要的 token。常见做法是不要在 worker 上频繁换号登录改成在 App Store Connect 后台生成 API Key把.p8文件权限配成600并在环境变量里注入 Key ID 和 Issuer ID。V2 系统如果内置签名调用通常会预留GK_API_KEY这类配置项。5.2 用命令行把问题捞出来排错不能只靠页面提示下面的命令组合基本能覆盖从描述文件到下载链路的全部状态。# 解开描述文件确认设备列表和允许的证书 security cms -D -i build/embedded.mobileprovision -o profile.plist /usr/libexec/PlistBuddy -c Print :ProvisionedDevices profile.plist # 检查下载链接响应头和文件大小 curl -I https://sign.example.com/manifests/demo.plist curl -I https://sign.example.com/apps/demo.ipa # 看 iOS 安装阶段日志 log show --last 5m --style compact \ --predicate process mobileassetdlog show是 macOS 侧看 iOS 安装日志的辅助方法真正的系统端日志在手机设置里导出。如果你在 macOS 上做签名 worker重点看process installd和process mobileassetd两段。IPA 下载完成后设备会短暂显示“正在验证”这时 installd 的日志会明确写出描述文件设备不匹配或证书无效。5.3 三个必调参数和它的作用第一PHP 上传限制。一个开发版 IPA 可能四五百 MB如果后台用的是 PHP默认post_max_size 8M上传一个三分钟视频大小的包就直接失败。至少调到 2G同时改 Nginx 的client_max_body_size 2g。这两个参数不配套上传会在切到一半时断掉。upload_max_filesize 2G post_max_size 2G max_execution_time 300第二签名队列并发数设为 1。很多开源 V2 系统在文档里没标但多个签名任务同时执行会争抢同一个钥匙串出现Security: Crl error或者签名结果不完整。用 Redis 队列时把queue:work的--tries3和--stop-when-empty配好保留一个 worker 即可。第三embedded.mobileprovision过期日的自动提醒。描述文件不是永久的企业证书一般一年有效Ad Hoc 更短。系统里如果只存文件不存过期时间等用户批量安装失败才排查就晚了。建议在数据库里给 profile 表加一个expires_at字段并在后台仪表盘用红色标注开源系统没做到的话自己加一个字段并不难。6. 进阶把签名下载页接进 iOS 系统原生分享和 CI6.1 下载页加一个系统原生分享按钮签名网站的下载页通常只提供一个大按钮用户拿到链接后用微信或邮件转发给测试同事但微信里点击itms-services链接经常被内置浏览器拦截。更稳的方式是收起后自己把链接放进剪贴板或者直接唤起 iOS 的系统分享面板。Web Share API 是 iOS 12.2 以后就原生支持的能力一个短函数就能把链接送进“隔空投送/信息/邮件”的分享列表。async function shareAppLink() { const manifestUrl encodeURIComponent( https://sign.example.com/manifests/demo.plist ); const installUrl itms-services://?actiondownload-manifesturl manifestUrl; const shareData { title: Demo App, text: 新版本内测包点击安装, url: installUrl }; if (navigator.share) { try { await navigator.share(shareData); } catch (e) { // 用户取消分享安静退出即可 } } else { navigator.clipboard.writeText(installUrl); } }safari对navigator.share的支持很完整Chrome iOS 也有。要注意shareData.text不要写太长否则分享面板会被截断。点击按钮后如果系统弹出原生分享面板说明 iOS 系统原生分享已经被正确唤起这是网页里最可靠的转发路径。6.2 把签名网站接入 CI 打包流程V2 系统的 API 设计得好的话管理后台本身就提供上传接口。把下面这段 curl 放到 Xcode 打包之后的脚本里就能在每次构建结束自动生成一个可分享的内容链接。curl -F filebuild/Demo.ipa \ -F token$SIGN_TOKEN \ -F bundle_idcom.example.demo \ -F version1.3.0 \ https://sign.example.com/api/v2/upload # 返回示例{code:0,data:{manifest_url:https://sign.example.com/manifests/f3a9.plist}}在 GitLab CI 或 GitHub Actions 上执行时把$SIGN_TOKEN放到环境变量里不要写进仓库。拿到manifest_url后再调用企业微信机器人或飞书机器人把itms-services链接发到测试群里。这样整个流程从 Xcode 打包发布 iOS、上传签名系统、生成下载页到通知测试全部不需要人工打开网页操作内测效率比手动上传高一个量级。本文还有配套的精品资源点击获取