• 苹果手机通过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=" 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

  • 相关阅读:
    POJ 2594 Treasure Exploration(最大路径覆盖)
    POJ 2516 Minimum Cost(最小费用最大流)
    城市面积
    python strip()函数
    python sys.path.append
    python调用shell,python与shell间变量交互
    远程登陆强大命令screen
    pythonLevenshtein几个计算字串相似度的函数解析
    python 程序bug解决方案
    python 全局变量
  • 原文地址:https://www.cnblogs.com/captainad/p/10874696.html
Copyright © 2020-2023  润新知