蘋果手機通過Safari瀏覽器訪問web方式安裝In-House應用


需求背景

公司內部員工使用的iOS客戶端應用希望對內開放,不需要發布於AppStore直接能夠讓內部用戶獲取,對於Android應用來說這個問題很好解決,直接下發安裝包然后就能安裝了;但是對於蘋果生態來說,這種方式是行不通的,因為蘋果本身有一套完備的應用安裝體系,除了具備一定特性之外的應用,都必須通過在AppStore上發布然后被用戶獲取。但是蘋果依然對企業內部應用(In-House應用)有所特別對待,即可通過web方式來獲取和安裝,那么我們需要做的,就是熟悉這一套實現流程。

開發准備

本項目主要說明后台服務端實現,前期還有很多准備工作,可能涉及到的是蘋果開發者賬號、企業證書生成、企業證書簽名的ipa、應用相關的bundle-identifier等,這些事項基本都是iOS客戶端開發同學來操作的,后台項目需要用到的內容都可以找他們提供。

要點說明

iOS APP

1、必須是由$299購買的企業證書簽名過的In-House應用,$99購買的證書簽名是無效的。

2、需要提供應用或者證書相關的bundle-identifier信息,因為plist中需要使用。

plist

1、plist文件必須使用固定且完整的xml格式。

2、plist文件中的ipa文件路徑無須是https協議下的。

3、plist文件必須通過https協議訪問,而且是蘋果受信任的企業證書。

方案步驟

1、通過web后台來管理和維護iOS版本。

2、web后台提供iOS應用的上傳功能,上傳的同時生成和app配套的plist文件。

3、app文件上傳成功,web后台維護記錄成功之后,會得到safari瀏覽器訪問的路徑。

4、Safari瀏覽器訪問到獲取應用的路徑之后會打開下載頁面,點擊按鈕是通過itms-services協議訪問的plist文件。

5、訪問該文件之后,手機將會自動彈窗提示當前網站想要安裝XXX應用。

6、安裝應用完成之后,首次嘗試打開應用時,系統會提示該應用未受信任,需要前往手機「設置-通用-描述文件與設備管理」下信任該應用,信任之后將可以正常打開和使用。

功能開發

1、web后台上傳和維護app應用(展開以顯示代碼)

 1 <!-- Captain&D -->
 2 <!-- https://www.cnblogs.com/captainad/ -->
 3 <div class="modal inmodal" id="myModal_editApp" tabindex="-1" role="dialog" aria-hidden="true">
 4     <div style="width: 1000px" class="modal-dialog">
 5         <div class="modal-content animated bounceInRight">
 6             <div class="modal-header">
 7                 <button type="button" class="close" data-dismiss="modal"><span
 8                         aria-hidden="true">&times;</span><span class="sr-only">關閉</span>
 9                 </button>
10                 <h5 class="modal-title" id="configTitle" data-lang="">增加/修改應用版本</h5>
11                 <input type="hidden" id="versionId" >
12                 <input type="hidden" id="appTypeId" >
13             </div>
14             <div class="modal-body">
15                 <div class="row">
16                     <div class="col-sm-6">
17                         <div class="form-group">
18                             <label>對外版本號</label>
19                             <input type="text" id="versionName" class="form-control" placeholder="下載時顯示的apk名稱,無需加.apk后綴">
20                         </div>
21                         <div class="form-group">
22                             <label>對內版本號</label>
23                             <input type="text" id="versionCode" class="form-control">
24                         </div>
25                         <div class="form-group">
26                             <label id="appfile_title">應用文件</label>
27                             <div id="file-pretty">
28                                 <div class="form-group">
29                                     <input type="file" name="accountFile" id="appfile" class="form-control" >
30                                 </div>
31                             </div>
32                         </div>
33                         <div class="form-group">
34                             <label>發布版本</label>
35                             <div class="checkbox checkbox-success">
36                                 <input id="checkbox2" type="checkbox">
37                                 <label for="checkbox2">
38                                     勾選並保存修改之后,當前版本將發布成博客原創Captain&D在線可用的最新版本
39                                 </label>
40                             </div>
41                         </div>
42                         <div class="form-group">
43                             <label>是否強制升級</label>
44                             <div class="checkbox checkbox-success">
45                                 <input id="checkbox4" type="checkbox">
46                                 <label for="checkbox4">
47                                     當前版本啟用之后,用戶打開客戶端后會立即強制升級成博客原創Captain&D當前版本
48                                 </label>
49                             </div>
50                         </div>
51                     </div>
52                     <div class="col-sm-6">
53                         <div class="form-group">
54                             <label>升級日志</label>
55                             <textarea class="form-control" id="upgradeLog" rows="12" style="resize: none"></textarea>
56                         </div>
57                     </div>
58                 </div>
59                 <div class="row">
60                     <p style="color:red;display: none" id="errMsg">
61                     </p>
62                 </div>
63             </div>
64             <div class="modal-footer">
65                 <button type="button" class="btn btn-success" id="saveEdit" >保存</button>
66                 <button type="button" class="btn btn-white" data-dismiss="modal" data-lang="close">關閉</button>
67             </div>
68         </div>
69     </div>
70 </div>
View Code

2、從頁面上傳附件相關處理方式(展開以顯示代碼)

 1 <!-- Captain&D -->
 2 <!-- https://www.cnblogs.com/captainad/ -->
 3 $("#saveEdit").click(function () {
 4     if(validateParam()) return;
 5 
 6     // 先進行存在性校驗
 7     var formdate = new FormData();
 8     formdate.append('id', $("#versionId").val());
 9     formdate.append('versionName', $("#versionName").val());
10     formdate.append('versionCode', $("#versionCode").val());
11     $('#loading-modal').modal("show");
12     $.ajax({
13         url: "versionmng/existsSameAppVersion",
14         type: "post",
15         data: formdate,
16         processData : false,
17         contentType : false,
18         success: function(data1){
19             if(data1.code == 200) {
20 
21                 // 正式發起保存請求
22                 var checked = $("#checkbox2").is(':checked');
23                 var checked1 = $("#checkbox4").is(':checked');
24                 var formdate = new FormData();
25                 var fils = $("#appfile").get(0).files[0];
26                 console.log(fils);
27                 formdate.append('appFile', fils);
28                 formdate.append('id', $("#versionId").val());
29                 formdate.append('appType', $("#appTypeId").val());
30                 formdate.append('versionName', $("#versionName").val());
31                 formdate.append('versionCode', $("#versionCode").val());
32                 formdate.append('upgradeLog', $("#upgradeLog").val());
33                 formdate.append('appStatus', checked ? 1 : 0);
34                 formdate.append('forcedUpgrade', checked1 ? 1 : 0);
35 
36                 $.ajax({
37                     url: "versionmng/addAppVersion",
38                     type: "post",
39                     data: formdate,
40                     processData : false,
41                     contentType : false,
42                     success: function(data){
43                         if(data.code == 200) {
44                             $("#myModal_editApp").modal("hide");
45                             $("#errMsg").html("");
46                             $("#errMsg").css("display", "none");
47                             swal("Successfully", "新增/修改App應用版本信息博客原創Captain&D成功", "success");
48                             initload(pageObj);
49                         }else {
50                             swal("Failed", data.msg, "error");
51                         }
52                         $('#loading-modal').modal("hide");
53                     }
54                 });
55 
56             }else {
57                 swal("Failed", data1.msg, "error");
58                 $('#loading-modal').modal("hide");
59             }
60         }
61     });
62 })
View Code

3、Captainad通過上傳資源到雲服務器的方法(展開以顯示代碼)

 1 /**
 2 * 增加應用版本
 3 * Captain&D
 4 * https://www.cnblogs.com/captainad/
 5 */
 6 public Result addAppVersion(HttpServletRequest request, @RequestParam(value = "appFile", required = false) MultipartFile file) {
 7 
 8     ···
 9 
10     // 文件處理
11     if(file != null && file.getSize() > 0) {
12         // 檢查文件類型
13         String filename = file.getOriginalFilename();
14         String suffix = filename.substring(filename.lastIndexOf("."), filename.length());
15         log.info("file format: {} {}", filename, suffix);
16         if ("1".equals(appType) && !".apk".contains(suffix) || "2".equals(appType) && !".ipa".contains(suffix)) {
17             return Result.builder()
18                     .code(ResultLanguage.getResultMessage(ResultMessage.WRONG_APP_FORMAT).getCode())
19                     .msg(ResultLanguage.getResultMessage(ResultMessage.WRONG_APP_FORMAT).getMsg()).build();
20         }
21         String appName = "";
22         if("1".equals(appType)) {
23             appName = versionName.replace(" ", "_").replace(".apk", "").concat(".apk");
24         }else {
25             appName = versionName.replace(" ", "_").replace(".ipa", "").concat(".ipa");
26         }
27 
28         try{
29             Map<String, String> fileMap = fileOperationService.uploadFile(appName, "/captainad/app/", file.getInputStream());
30             if(null != fileMap && !fileMap.isEmpty()) {
31                 for(Map.Entry<String, String> set : fileMap.entrySet()) {
32                     String downloadUrl = set.getKey();
33                     String appMd5 = set.getValue();
34                     requestMap.put("downloadUrl", new String[]{downloadUrl});
35                     requestMap.put("appMd5", new String[]{appMd5});
36                 }
37             }else {
38                 return Result.builder()
39                         .code(ResultLanguage.getResultMessage(ResultMessage.APP_UPLOAD_ERROR).getCode())
40                         .msg(ResultLanguage.getResultMessage(ResultMessage.APP_UPLOAD_ERROR).getMsg()).build();
41             }
42         }catch (Exception e) {
43             log.error("上傳客戶端App文件存在異常。", e);
44         }
45     }
46 }
View Code

4、通過拼接字符串生成plist文件

 1 /**
 2 * 生成iOS應用對應的plist文件
 3 * Captain&D
 4 * https://www.cnblogs.com/captainad/
 5 */
 6 private String genIosPlist(CaptainadAppVersionInfo captainadAppVersionInfo){
 7     StringBuilder builder = new StringBuilder();
 8     builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
 9     builder.append("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
10     builder.append("<plist version=\"1.0\">");
11     builder.append("<dict>");
12     builder.append("    <key>items</key>");
13     builder.append("    <array>");
14     builder.append("        <dict>");
15     builder.append("            <key>assets</key>");
16     builder.append("            <array>");
17     builder.append("                <dict>");
18     builder.append("                    <key>kind</key>");
19     builder.append("                    <string>software-package</string>");
20     builder.append("                    <key>url</key>");
21     builder.append("                    <string>").append(captainadAppVersionInfo.getDownloadUrl()).append("</string>");
22     builder.append("                </dict>");
23     builder.append("            </array>");
24     builder.append("            <key>metadata</key>");
25     builder.append("            <dict>");
26     builder.append("                <key>bundle-identifier</key>");
27     builder.append("                <string>").append(getSetCacheService.getConfigValue("ios_bundle_identifier")).append("</string>");
28     builder.append("                <key>bundle-version</key>");
29     builder.append("                <string>").append(captainadAppVersionInfo.getVersionCode()).append("</string>");
30     builder.append("                <key>kind</key>");
31     builder.append("                <string>software</string>");
32     builder.append("                <key>title</key>");
33     builder.append("                <string>Captainad App</string>");
34     builder.append("            </dict>");
35     builder.append("        </dict>");
36     builder.append("    </array>");
37     builder.append("</dict>");
38     builder.append("</plist>");
39     String plistName = captainadAppVersionInfo.getVersionName().concat(".plist");
40     try {
41         InputStream is = new ByteArrayInputStream(builder.toString().getBytes("UTF-8"));
42         Map<String, String> fileMap = fileOperationService.uploadFile(plistName, "/captainad/app/plist/", is);
43         if(null != fileMap && !fileMap.isEmpty()) {
44             for(Map.Entry<String, String> entry : fileMap.entrySet()) {
45                 log.info("生成的plist的文件地址:{}", entry.getKey());
46                 return entry.getKey();
47             }
48         }
49     } catch (Exception e) {
50         log.error("生成plist文件時出現異常。", e);
51     }
52     return null;
53 }

5、數據庫表設計(展開以顯示代碼)

 1 -- Captain&D
 2 -- https://www.cnblogs.com/captainad/
 3 CREATE TABLE `captainad_app_version_info` (
 4   `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
 5   `version_name` varchar(64) DEFAULT NULL COMMENT '外部版本號',
 6   `version_code` varchar(64) DEFAULT NULL COMMENT '內部版本號',
 7   `upgrade_log` text COMMENT '更新日志',
 8   `download_url` varchar(128) DEFAULT NULL COMMENT '版本路徑',
 9   `app_md5` varchar(32) DEFAULT NULL COMMENT '文件MD5',
10   `app_status` int(11) DEFAULT NULL COMMENT '版本狀態(0-關閉,1-啟用)',
11   `release_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '發布時間',
12   `create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
13   `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改時間',
14   `forced_upgrade` int(11) DEFAULT '0' COMMENT '是否強制升級(0-否,1-是)',
15   `app_type` int(11) DEFAULT NULL COMMENT '應用類型(1-Android,2-iOS)',
16   PRIMARY KEY (`id`)
17 ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='App版本管理';
View Code

6、safari通過訪問路徑之后的路由處理(展開以顯示代碼)

 1 /**
 2 * 進入App下載安裝頁面
 3 * Captain&D
 4 * https://www.cnblogs.com/captainad/
 5 */
 6 @AuthorityVerify
 7 @RequestMapping("ios")
 8 public String toDownloadIosAppPage(HttpServletRequest request) {
 9     String version = request.getParameter("version");
10     String httpsHost = getSetCacheService.getConfigValue("file_cloud_visit_host_https");
11     String plistUrl = httpsHost.concat("/captainad/app/plist/").concat(version).concat(".plist");
12     request.setAttribute("plist", plistUrl);
13     return "/appmng/ios_app";
14 }
View Code

7、應用下載頁面的plist路由協議寫法

 1 <!-- Captain&D -->
 2 <!-- https://www.cnblogs.com/captainad/ -->
 3 <!-- 下載安裝in-house應用關鍵代碼 -->
 4 <div class="wrapper wrapper-content">
 5     <div class="row">
 6         <div class="col-sm-12">
 7             <div class="middle-box text-center animated fadeInRightBig" style="margin-top: 90%;">
 8                 <!--<h3 class="font-bold">這里是頁面內容</h3>-->
 9 
10                 <div class="install-btn">
11                     <br/><a href="itms-services://?action=download-manifest&url=${plist}" class="btn btn-success btn-lg m-t">
12                     <i class="fa fa-apple"></i>  Install Tesla app for iOS</a>
13                 </div>
14             </div>
15         </div>
16     </div>
17 </div>

圖片參考

1、應用列表

2、應用詳情

3、掃描安裝圖示(項目暫時無法截圖,故參考自網絡,打碼處理,侵刪)

4、信任應用(項目暫時無法截圖,故參考自網絡,打碼處理,侵刪)

遇到問題及解決思路和方法

1、Safari點擊之后出現無法連接到xxx.xx.com現象。

  • 檢查下發的plist文件能否訪問。
  • 詢問Https證書是否是有效的並且受信任的。
  • 檢查訪問的plist文件的鏈接是否是https協議的。
  • 檢查下發的plist文件xml格式是否正常,可以在線格式化下,看是否報錯。

2、能夠連接但是無法下載安裝。

  • 檢查plist文件中鏈接的ipa文件是否可達。
  • 檢查文件格式是否為ipa,檢查ipa文件名與plist文件名是否一致。

參考資料

1、https://www.jianshu.com/p/89d22b430330

2、https://www.cnblogs.com/star91/p/5018995.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM