获取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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!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信息转送到用户手机上显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?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网页对象的下载链接。

1
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格式的信息,还有一些署名后生成的二进制数据。大致是这个样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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来实现在用户端显示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<?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,随便写了个如下:

1
2
3
4
5
6
7
8
9
10
<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>

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