获取iOS设备UDID信息

最近项目上需要发布测试版iOS app,决定使用Ad Hoc方式小范围发布,这就需要事先获取目标设备的UDID 字符串才可以。本文记录一下采用OTA profile的方式来获取UDID信息的各个步骤。

先简单介绍一下iOS的测试版发布方法。由于Apple的严格限制,导致iOS app无法像安卓那样,直接将编译好的二进制文件提供给测试者,测试人员安装后执行就可以了。Apple规定,目前可以采用如下几种方法来发布测试版app:

  • 企业版开发者用户可以直接把编译好的测试版ipa文件放在网页上,用户用手机访问该链接即可自动下载,安装并运行。这个企业版开发者账号年费399刀,但不能发布到App Store。此方法简单易行,测试者只要知道该链接即可进行下载测试,缺点是费用稍高,而且需要注册企业版用户,还用额外准备公司相关材料供Apple审核,手续繁琐。
  • 普通开发者用户(99美金一年)可以发布到App Store,也可以通过Test flight或者Ad Hoc这两种方式发布测试版App。 Test flight要求你先上传你的app,然后经过苹果的人工审核通过后,就可以发布出去给目标测试用户。目标测试用户会收到邮件提示,按照邮件提示的链接在手机上安装Test Flight 后打开,就能点击下载测试版ipa文件运行。此法的好处是用户不需要太多操作就可以运行,缺点是需要等待Apple审批,无法迅速发布给测试用户。而Ad Hoc方式就可以快速发布编译好的ipa文件并挂在自己网页上,和企业版用户类似,测试用户也可以通过该链接直接下载并安装运行。但这里有个缺陷,Ad Hoc要求在编译该ipa时就指定哪些设别可以运行此测试版本,这是通过实现注册好相关设备的UDID来实现的,因此如果想在新的手机上也运行测试版,那么就需要先设法获取设别的UDID,然后更新profile和certificate,再用这更新后的信息编译并获取新的ipa,然后重新上传。好处是不需要Apple参与审核,一经发布即可测试。

因为Apple隐藏了直接获取UDID的API,我们需要通过OTA Profile的方法来实现。因为用户需要通过访问一个网页来下载此Profile文件,而这个网页需要通过Apache或者Nginx来提供访问,这里具体的Apache或者Nginx操作略。

第一步,准备一个HTML网页,它提供一个下载链接,该链接指向下一步要生成的OTA profile 文件(这里举例说明,profile文件名我们起名为为enroll.mobileconfig)

<!doctype html>
<html lang="en">

<head>
	<title>Get iOS Device UDID</title>	
</head>

<body>
	<h2>Find your iOS device's UDID</h2>
	<div>
		<a href="enroll.mobileconfig">Click to find device UDID</a>		
	</div>

</body>

</html>

第二步,手动创建上面这个enroll.mobileconfig文件,它的名字自定义,可以改。它是一个xml文件,具体可参照如下。注意DeviceAttributes这个key对应的值是一个数组,包含了所有我们想获取的值(近期Apple不断收紧隐私政策从而不再提供某些值)。有些属性名字关键字是固定的,不能动。而那个URL,则应该指向一个网页用来接受iOS提供的设备信息,这里URL我们设计了一个php文件,它将会被用来接受返回的xml信息并再次把XML信息转送到用户手机上显示。

<?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>yourwebsite.com/GetUDID/done.php</string>
            <key>DeviceAttributes</key>
            <array>
            <string>UDID</string>
			<string>IMEI</string>
			<string>MEID</string>
			<string>ICCID</string>
			<string>VERSION</string>
			<string>PRODUCT</string>
			<string>MAC_ADDRESS_EN0</string>
			<string>DEVICE_NAME</string>
			<string>SERIAL</string>
			<string>IMSI</string>
			<string>ECID</string>   
            </array>
        </dict>
        <key>PayloadOrganization</key>
        <string>Your company name(自定义,它会显示给用户)</string>
        <key>PayloadDisplayName</key>
        <string>iOS Profile Service(自定义,它会显示给用户)</string>
        <key>PayloadVersion</key>
        <integer>1</integer>
        <key>PayloadUUID</key>
        <string>D84C2DBF-2E70-4D91-A55A-4AB4D56C1234(自定义,global unique,全球唯一就可以)</string>
        <key>PayloadIdentifier</key>
        <string>com.youwebsite.profile.service(自定义,用于区分不同profile)</string>
        <key>PayloadDescription</key>
        <string>This profile is used to get device UDID info to test Blizzz's iOS app.(自定义,它会显示给用户)</string>
        <key>PayloadType</key>
        <string>Profile Service</string>
    </dict>
</plist>

第3步,如果不sign上面这个证书文件,也可以使用,但是用户端会提示这个证书签名未确认,可能会增加用户疑虑,因此这一步可以考虑sign这个文件。和https认证类似,这个signature过程也需要使用到该web服务器上的几个SSL证书,具体是执行一个命令,使用这几个SSL证书对上面手动创作的那个enroll.mobileconfig进行处理,得到一个新的文件,然后这个新的文件用来替换掉上面那个原始的enroll.mobileconfig,那样用户端就会看到该证书已经被确认。这里我的机器使用了let‘s encrypt提供的SSL证书,因此命令具体如下,署名后的文件名是signed.enroll.mobileconfig,我们记得要把这个文件更新到上面html网页对象的下载链接。

sudo openssl smime -sign -in enroll.mobileconfig -out signed.enroll.mobileconfig -signer /etc/letsencrypt/live/YOUWEBSITE.com/fullchain.pem -inkey /etc/letsencrypt/live/YOUWEBSITE.com/privkey.pem -certfile /etc/letsencrypt/live/YOUWEBSITE.com/fullchain.pem -outform der -nodetach

第4步,准备一个php文件用来接受iOS返回的设备信息。用户点击按钮并下载该profile证书后,会提示它进入系统设置来找到这个刚下载完的证书并安装。按照提示安装和运行就行,这样iOS系统就会返回一个XML格式的数据流,里面包含了一个XML格式的信息,还有一些署名后生成的二进制数据。大致是这个样子:

xyz..a&*&^**(( 前面是一些乱码字符,由署名产生)

<?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>IMEI</key>
	<string>35 00......</string>
	<key>MEID</key>
	<string>35056......</string>
	<key>PRODUCT</key>
	<string>iPhon......</string>
	<key>SERIAL</key>
	<string>HXY4......</string>
	<key>UDID</key>
	<string>000081......</string>
	<key>VERSION</key>
	<string>19......</string>
</dict>
</plist>

!@#¥@xyz..a&*&^**(( 后面是一些乱码字符,由署名产生)

我们只关心中间的XML部分,因为它包含了我们所需的各种信息。这里我们要写一个PHP代码来实现接收该数据流并转动到另一个URL来实现在用户端显示。

<?php


$data = file_get_contents('php://input');
//剔除前面和后面多余的乱码字符
$plistBegin   = '<?xml version="1.0"';
$plistEnd   = '</plist>';
$pos1 = strpos($data, $plistBegin);
$pos2 = strpos($data, $plistEnd);
$data2 = substr ($data,$pos1,$pos2-$pos1+8);
echo ($data2);//data2 contains all xml content

//file_put_contents("result.txt", $data2);//保存成文件用于调试

//开始解析XML并拿到各值
$xml = xml_parser_create();
xml_parse_into_struct($xml, $data2, $vs);
xml_parser_free($xml);

$UDID = "";
$CHALLENGE = "";
$DEVICE_NAME = "";
$DEVICE_PRODUCT = "";
$DEVICE_VERSION = "DEVICE_VERSION";

$iterator = 0;

$arrayCleaned = array();
foreach($vs as $v){
    if($v['level'] == 3 && $v['type'] == 'complete'){
    $arrayCleaned[]= $v;
}
    $iterator++;
}

$data = "";
$iterator = 0;

foreach($arrayCleaned as $elem){

$data .= "\n==".$elem['tag']." -> ".$elem['value']."<br/>";

switch ($elem['value']) {

case "PRODUCT":

$DEVICE_PRODUCT = $arrayCleaned[$iterator+1]['value'];

break;

case "UDID":

$UDID = $arrayCleaned[$iterator+1]['value'];

break;

case "VERSION":

$DEVICE_VERSION = $arrayCleaned[$iterator+1]['value'];

break;

}
$iterator++;
}

$params = "UDID=".$UDID."&DEVICE_PRODUCT=".$DEVICE_PRODUCT."&VERSION=".$DEVICE_VERSION;
//关键,强制301跳转到另一个URL并把解析出的各属性值作为URL请求参数带入
header('Location: https://YOUWEBSITE.com/GetUDID/complete/index.php?'.$params, true, 301);

?>

第5步,准备另一个php文件用来接受上面的跳转到命令传递过来的参数。这里上面例子里写了个complete/index.php,随便写了个如下:

<html>
	<body>
		<h2>Here is the device information.</h3>
		<form name="myForm" style="font-size: 1.5em;">
		  <label for="POST-udid">UDID:</label><br/><input readonly id="POST-udid" type="text" value="<?php echo $_GET['UDID'] ?>" size="36" ><br/><br/>
		  <label for="POST-version">Version:</label><br/><input readonly id="POST-version" type="text"  value="<?php echo $_GET['VERSION'] ?>" size="36"><br/><br/>
		  <label for="POST-product">Product:</label><br/><input readonly id="POST-product" type="text"  value="<?php echo $_GET['DEVICE_PRODUCT'] ?>" size="36"><br/><br/>
		</form>
	</body>
</html>

至此,用户在手机上确认安装完该证书后,就可以直接跳转到浏览器并显示返回的各种数据了