简介:这份资源是一套完整的iOS超级签名系统源码,面向需要长期稳定分发苹果应用的开发者、内测平台运营者及企业签名服务从业者。针对企业证书频繁掉签、设备注册流程繁琐等痛点,系统基于个人开发者账号实现365天不掉签,通过自动化集成Apple API完成设备注册与描述文件更新,支持多证书智能轮换、UDID自动获取、zsign免Mac签名,并配套宝塔面板一键部署与中英文游戏风格下载页。资源共46个文件,以30个PHP核心业务文件为主,另含SQL数据库、Shell部署脚本、zsign签名工具、MobileProvision配置文件、Nginx配置示例及README/INSTALL等文档,压缩包整体仅1.33MB。已有131人学习下载。获取后可得到可直接部署的后端接口、设备UDID校验逻辑、证书轮换管理模块、安装页面模板及日志调试记录,适合具备基础PHP开发能力、希望快速搭建自有签名分发系统的开发者对照实践。
1. 告别企业签掉签:超级签名源码解决的不只是掉签问题
做 iOS 分发的朋友应该都经历过这种场面:半夜用户截图说 App 打不开了,点开一看提示“无法验证App”,后台里企业证书状态变成了 Revoked。企业签的掉签不是偶然,是证书被苹果检测到滥用后的必然结果,掉一次就要重新签名、重新发链接,用户还得手动删掉旧证书才能装上。我最早接触超级签名,就是被企业签这种“随时可能清零”的状态逼的。超级签名本质上是拿个人开发者账号(99 美元一年的那种)按设备维度做签名:一个账号最多注册 100 台设备,每一台设备拿到只属于自己的描述文件和 IPA,掉签只影响单台设备,不会像企业签那样一崩全崩。这套 iOS 签名系统源码把 UDID 捕获、设备注册、按设备重签、itms-services 分发整条链路串起来了。适合正在用企业签但被掉签困扰的开发者,也适合想自建分发通道、不想把用户数据交给第三方平台的运维。本文从签名原理讲到具体搭建步骤,再把容易翻车的几个点拿出来单独说。
2. 签名原理与选型:企业签、TestFlight、超级签名的取舍
2.1 三种签名方式的成本与风险差异
先说清楚企业签为什么掉签,这决定了你为什么要换方案。企业签用的是 In-House 证书,苹果发这张证书的初衷是给企业内部员工分发用的,不限设备数量。但国内很多分发平台把它变成了“只要花钱就能签”的公共通道,同一张证书上挂了几万台设备、几十个 App,苹果的风控模型一查一个准,直接吊销证书,于是所有人一起掉签。这就是企业签的命门:它省事,但风险完全不可控。
TestFlight 是苹果官方渠道,设备上限一万,但每一版都要走 Beta 审核,不能作为日常分发手段,适合正式对外测试,不适合那种“今天发版今天就要装”的场景。
超级签名走的是个人开发者账号,一个账号 100 台设备,每台设备的 UDID 被单独写进描述文件,IPA 也只对这台设备签名。好处是掉签范围被限制在单个设备,坏处是要自己处理整套自动化流程。三者对比如下:
| 方案 | 年成本 | 设备上限 | 掉签风险 | 适合场景 |
|---|---|---|---|---|
| 企业签 | 证书渠道几千到上万 | 不限 | 高,一崩全崩 | 不差钱、临时分发 |
| TestFlight | 99 美元 | 10000 | 无,但要过审 | 正式对外测试 |
| 超级签名 | 99 美元/账号 | 100/账号 | 低,单设备受影响 | 内测、定向分发 |
2.2 UDID、描述文件与重签名的关系
超级签名的核心技术点只有三个:UDID、描述文件(mobileprovision)、重签名。UDID 是每台 iOS 设备的唯一标识,苹果不允许 App 直接读取它,但有一个合法通道可以拿到——让用户在手机浏览器里安装一个配置描述文件,系统会把 UDID 以回调的形式发给你指定的服务器。
描述文件是 Apple 用来绑定“证书 + 设备白名单 + App 权限”的签名文件。常规分发里一个描述文件包含一百个设备 UDID,而超级签名的做法是为每台设备生成一个只包含它自己的描述文件。这样做的好处是从根上隔离风险:设备 A 的包被人拿去逆向、滥用,设备 B、C、D 完全不受影响。
重签名就更好理解了:你手里有一个已经打包好的 IPA,它原来可能是用开发者证书签的,也可能签过企业证书,现在需要用超级签名账号的证书和这台设备的描述文件重新 codesign 一遍。落地的工具我用得最多的是 zsign,跨平台、命令行操作、比 Xcode 里手动配置快得多。
2.3 超级签名的完整链路
整套系统的流程是这样的:用户打开你的下载页,点击“获取UDID”按钮,手机弹窗询问是否允许安装描述文件;用户确认后系统自动打开设置完成安装,同时浏览器向你的服务器发起一个带 UDID 参数的回调请求;服务器收到 UDID 后,先检查数据库里有没有这台设备,如果没有就调用开发者的 Apple API 把 UDID 注册进账号;注册成功后,服务器用这个账号的证书和刚生成的专属描述文件对 IPA 重新签名;签名完成后把新的 IPA 地址和下载 plist 返回给前端页面,用户点击就通过 itms-services 协议拉起系统安装器。
这里有一个关键点必须明白:描述文件必须在设备注册成功之后生成,不能提前。如果你先把描述文件生成好了再注册设备,描述文件里的 UDID 白名单就是空的,装到手机上一定报“证书无效”。很多第一次搭的人就在这一步翻车。
3. 后端服务与 UDID 捕获:从安装描述文件到回传设备信息
3.1 数据库设计:一台设备一条记录
后端服务是整个超级签名系统的大脑。先设计一张设备表,核心字段包括 UDID、设备名、注册状态、签名状态、IPA 地址。精确到单台设备的管理粒度,是超级签名区别于企业签的关键。
CREATE TABLE devices ( id INTEGER PRIMARY KEY AUTOINCREMENT, udid TEXT UNIQUE NOT NULL, device_name TEXT DEFAULT '', status TEXT DEFAULT 'pending', -- pending / registered / signed / revoked ipa_url TEXT DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_devices_udid ON devices(udid);这张表里udid字段加了 UNIQUE 约束,防止同一台设备被重复注册。status字段记录设备当前所处的阶段:刚回传 UDID 是pending,注册进开发者账号是registered,IPA 签名完成是signed。实际运维中我会定期扫描pending超过 10 分钟的记录,多半是回调成功但注册失败,需要人工介入。
3.2 动态生成 UDID 描述文件
用户点击“获取 UDID”时,服务器需要动态给他生成一个描述文件(后缀为 .mobileconfig),这个文件的 PayloadType 必须是Profile Service。苹果的机制是:手机安装这类描述文件后,会向配置文件里指定的 URL 发起 GET 请求,把设备信息作为查询参数带过去。
<?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> <key>PayloadContent</key> <dict> <key>URL</key> <string>https://sign.example.com/api/udid/callback</string> <key>DeviceAttributes</key> <array> <string>UDID</string> <string>VERSION</string> <string>PRODUCT</string> </array> </dict> <key>PayloadOrganization</key> <string>Your Company</string> <key>PayloadDisplayName</key> <string>Device Registration</string> <key>PayloadVersion</key> <integer>1</integer> <key>PayloadUUID</key> <string>7A3B8D01-8F6E-4D62-9C84-8E6B1E2A9C01</string> <key>PayloadIdentifier</key> <string>com.example.profile</string> <key>PayloadDescription</key> <string>用于获取设备UDID以便完成内测安装</string> <key>PayloadType</key> <string>Profile Service</string> </dict> </plist>这段 XML 里最关键的是URL字段——它就是服务器接收 UDID 的回调地址,必须是 HTTPS,否则 iOS 会直接拒绝发起请求。DeviceAttributes数组声明了你要回收的设备信息,UDID 和 VERSION(系统版本)基本够用。注意 iOS 16 之后系统对 IMEI 等敏感信息的回收做了限制,你就算在数组里写 IMEI 也拿不到,不建议依赖它做设备标识。
3.3 接收 UDID 回调并触发设备注册
描述文件安装完成后,用户的手机浏览器会跳转回你配置的URL,以 GET 方式携带 UDID、VERSION 等参数。后端接口需要做三件事:解析 UDID、写入数据库、触发注册流程。我一般用 Flask 写这个回调接口:
from flask import Flask, request, jsonify import sqlite3, subprocess, uuid app = Flask(__name__) DB_PATH = "/data/supersign.db" @app.route("/api/udid/callback") def udid_callback(): udid = request.args.get("UDID") version = request.args.get("VERSION", "") product = request.args.get("PRODUCT", "") if not udid: return "missing udid", 400 conn = sqlite3.connect(DB_PATH) cur = conn.cursor() # 已存在则更新状态,不存在则插入 cur.execute("SELECT id FROM devices WHERE udid = ?", (udid,)) row = cur.fetchone() if row: cur.execute("UPDATE devices SET device_name=?, status='pending' WHERE udid=?", (product, udid)) else: cur.execute("INSERT INTO devices (udid, device_name, status) VALUES (?, ?, 'pending')", (udid, product)) conn.commit() conn.close() # 异步触发注册+签名,避免回调阻塞 subprocess.Popen(["/data/bin/register_device.sh", udid]) return "ok"这里把注册和签名做成了异步任务,调用外部脚本处理,避免用户浏览器一直等待。VERSION参数可以帮你判断设备系统版本,比如 iOS 16 以下和以上的设备在描述文件兼容性上有细微差别,后面避坑章节会细说。
3.4 设备注册与按设备重签名
设备注册是整套系统里最容易被卡住的一环。苹果没有提供公开的一键批量注册 API,常规做法是用 fastlane 的register_devices能力自动登录开发者后台完成注册。你需要一台 Linux 或 macOS 机器安装 fastlane,并配置好开发者账号环境变量。
# Fastfile lane :register_device do udid = ENV["DEVICE_UDID"] name = ENV["DEVICE_NAME"] register_devices( devices: { name => udid } ) end# 调用方式 export FASTLANE_USER="your@appleid.com" export FASTLANE_PASSWORD="your-password" export DEVICE_UDID="00008020-001D2A1C3E00401E" export DEVICE_NAME="iPhone 15 Pro" fastlane register_deviceregister_devices这个 action 会读取环境变量FASTLANE_USER和FASTLANE_PASSWORD登录 Apple 开发者后台,把传入的 UDID 注册进当前账号。这一步失败率最高的不是代码,而是 Apple ID 开启了双重认证——你需要额外处理会话 token,或者用 fastlane 内置的spaceauth先生成认证会话。
设备注册成功之后,就可以生成专属描述文件并对 IPA 重签名了。我用 zsign 做重签名,它不依赖 Xcode,在 Linux 上就能跑,适合部署在云服务器上。
#!/bin/bash # 单设备重签名脚本 /data/bin/resign_device.sh UDID=$1 PROFILE="/data/profiles/${UDID}.mobileprovision" IPA_SRC="/data/apps/original.ipa" IPA_OUT="/data/apps/${UDID}.ipa" # 用该设备的专属描述文件重新签名 zsign -k /data/certs/supersign.p12 \ -p "$CERT_PWD" \ -m "$PROFILE" \ -o "$IPA_OUT" \ "$IPA_SRC" \ -b com.example.app这段脚本里-k是开发者证书的 p12 文件,-p是证书密码,-m是刚才基于该 UDID 生成的描述文件,-b指定最终安装包的 Bundle ID。-o是输出路径,注意要为每台设备生成独立的输出文件,不能覆盖。整个流程跑通后,用户的安装包就是一个“只认它这一台设备”的专享 IPA。
4. 前端下载页与 itms-services 协议:让 iOS 浏览器唤起安装
4.1 HTTPS 与 ATS:不过这两关一切白搭
前端下载页承担了两个职责:第一步引导用户安装描述文件,第二步在签名完成后引导用户安装 App。iOS 9 之后 ATS(App Transport Security)强制要求所有网络请求走 HTTPS,配置描述文件的下载链接、回调接口、IPA 下载地址、plist 清单地址全部必须是 HTTPS。如果你用 HTTP,用户点击描述文件会直接提示“此连接不是私密连接”,压根走不到安装那一步。
实操里最容易忽略的是 IPA 下载地址也必须 HTTPS。很多人在服务器前面挂了 CDN 或者对象存储,结果桶的访问地址是 HTTP,安装时同样报错。我一般用 nginx 直接反代到本地签名目录,SSL 证书用免费的单域名证书就够。全部走 HTTPS 之后,再谈下一步。
4.2 下载页 HTML 与 itms-services 的启动方式
签名完成之后,页面需要生成安装按钮。iOS 安装 App 的合法方式是 itms-services 协议,它通过一个 plist 清单文件告诉系统 IPA 放在哪里、Bundle ID 是什么、应用名叫什么。点击按钮的标准写法是这样:
<a href="itms-services://?action=download-manifest&url=https%3A%2F%2Fsign.example.com%2Fplists%2F0C5A2B6C.mobileprovision.plist"> 安装内测版 </a>itms-services://是 iOS 系统注册的私有协议,url参数指向 plist 清单文件。这个 plist 的url字段必须是完整的 HTTPS 链接,系统拿到清单后才会去下载 IPA 并触发安装确认弹窗。有人图省事直接写 IPA 地址到<a>的 href 里,用户点击只会下载文件而不会拉起安装器,这就是安装不了的直接原因。
4.3 微信内打开失效:引导用户使用 Safari 唤起安装 App
ios 浏览器唤起安装 app 这件事,最容易栽在微信内置浏览器上。微信会拦截带有itms-services://前缀的链接,用户点击没有任何反应。解决方法是下载页检测到当前浏览器是微信时,展示一个遮罩引导用户“点击右上角菜单,在浏览器中打开”。这在技术上只需要一段简单的 UA 判断,但实践经验告诉我:引导文案一定要写在下载按钮上方,因为大多数用户点一次没反应就直接放弃了,根本不会往下看。
另一个容易被忽略的点:用户从 Safari 打开下载页安装描述文件,最后一步要回到设置里点“信任描述文件”。iOS 15 以上的系统对这个路径的提示做了优化,但新手用户还是会卡住。前端页面在“安装完描述文件后”要主动提示用户去 iOS 设置里允许描述文件。这个提醒做在页面上而不是写在文档里,能显著降低客服咨询量。
5. 超级签名搭建避坑清单:五条血泪经验
5.1 用户安装了描述文件,服务器却收不到回调
现象:用户明明是点了下载描述文件,设置里也显示已安装,但数据库里迟迟没有这条设备记录。原因分两种:要么是描述文件里的回调 URL 是 HTTP,被 ATS 拦截;要么是用户根本没在 Safari 里打开下载页,而是从微信、QQ 内置浏览器完成的安装,这两个浏览器对 Profile Service 协议支持很差。解决:确认回调地址是 HTTPS;描述文件下载链接强制引导到 Safari 打开。排错时我一般会在服务器上抓 nginx 访问日志,看有没有来自手机系统的请求——这里有个调试技巧,iOS 设备可以通过代理把流量指向 Fiddler 或 Charles,你就能看到系统在安装描述文件后到底有没有发出回调请求,以及返回的响应码。常见做法是直接在服务器上tail -f /var/log/nginx/access.log,过滤/api/udid/callback关键字。
5.2 安装时提示“无法安装App,因为证书无效”
现象:用户点击安装按钮,系统弹窗提示证书无效,安装进程直接中断。原因:最常见的是描述文件里没有这台设备的 UDID。典型场景是后端先签了 IPA 才注册设备,注册顺序反了;或者用户换了设备,用新 iPhone 安装之前签的包。解决:严格按“先注册 UDID → 再生成描述文件 → 最后重签 IPA”的顺序执行。我在后端代码里加了状态机,status不是registered的设备,前端根本不展示安装按钮,从流程上杜绝这个问题。另外还有一种情况是描述文件里的证书和 IPA 用 p12 证书不是同一张,zsign 签名时-k的证书必须和-m的描述文件出自同一个开发者账号。
5.3 zsign 重签后 App 打开就闪退
现象:安装成功,但点击图标后立刻闪退,后台日志什么关键错误都看不到。原因:App 的 entitlements 权限在重签名时丢了,最典型的是 Keychain Sharing、Push Notification、App Groups。原始 IPA 如果开启了这些能力,重签名后对应权限必须显式写入新描述文件。解决:zsign 签名前先导出原 IPA 的 entitlements。常见的做法是用ldid -e或者codesign -d --entitlements把权限声明提取出来,然后用 zsign 的-e参数指回。注意:如果原 App 用了推送,新签名描述文件里也必须包含 Push Notification 权限,否则就算签上了也无法注册推送。
5.4 100 台设备满员后,移除设备的冷却期
现象:账号设备数达到 100,想清理不活跃设备,结果移除后立刻添加新设备被苹果拒绝,提示“无法添加”。原因:苹果对开发者账号的设备管理有策略限制,一台设备从账号里移除后,通常要过 30 天左右才能重新注册进任意账号,这是苹果服务的硬限制,不是代码能绕过的。解决:维护设备活跃度,优先清理已经掉签超过 30 天且没有复装行为的设备。实践上我一般会记录每个 UDID 的last_active时间,清理时只选最近 60 天都没有任何安装记录的老设备。不要等满员才想起来清理,定期清才能保证名额始终有冗余。
5.5 老设备安装描述文件失败,提示“描述文件无效”
现象:iPhone 6、iOS 12 的设备,点击下载描述文件直接提示无效,Safari 没有弹出安装确认框。原因:描述文件里包含了老系统不认识的字段或配置项。iOS 12 和 iOS 17 对描述文件的解析器有差异,部分新字段在老系统上会直接报错。解决:在生成 mobileconfig 时对VERSION字段做判断,老系统返回精简版描述文件,只保留URL、DeviceAttributes、PayloadType这几个核心字段,去掉所有出厂版本高的配置项。排错时可以先用 mac 上的plutil -p检查生成文件的属性列表结构,格式一旦不合法,任何系统版本都装不了。
6. 进阶:证书到期自检与掉签自动恢复
超级签名解决了批量掉签,但你依然要盯着两件事:开发者证书到期,以及账号证书被苹果吊销。被吊销只能换账号重签,这是商业风险问题;但证书到期是能提前预警的,不用等用户报障。我习惯把每张描述文件当成一个“黑匣子”,定期打开它看有效期。描述文件是 CMS 签名包裹的 plist,需要用 security 命令先解开再读取:
import subprocess import plistlib from datetime import datetime # 解开描述文件签名,输出 plist out = subprocess.check_output( ["security", "cms", "-D", "-i", "embedded.mobileprovision"] ) profile = plistlib.loads(out) expire = profile.get("ExpirationDate") days_left = (expire - datetime.now()).days print(f"证书剩余 {days_left} 天") if days_left < 15: print("告警:描述文件即将到期,请准备续期")这个脚本放到 crontab 里每周跑一次,邮箱收到告警再去换证书。不要等到用户反馈打不开了才查证书,那说明掉签已经发生了。
另外一个习惯是给安装接口做心跳检测。我维护了一个下载页埋点,用户每次点“安装”时后端都会记录当前 IPA 的签名状态;如果连续出现“证书无效”类反馈,自动触发重签脚本对失败设备重新走一遍注册、签名、下发链路。从那以后我每次发布新包都会强制走一遍“新设备注册 → 安装描述文件 → 点击安装 → 确认不闪退”的全流程,而不是只看日志里签名成功就收工。这个习惯帮我挡住了至少三次半夜掉签的事故。希望帮到你。
本文还有配套的精品资源,点击获取