苹果手机通过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应用(展开以显示代码)
<!-- Captain&D -->
<!-- https://www.cnblogs.com/captainad/ -->
<div class="modal inmodal" id="myModal_editApp" tabindex="-1" role="dialog" aria-hidden="true">
<div style="width: 1000px" class="modal-dialog">
<div class="modal-content animated bounceInRight">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal"><span
aria-hidden="true">×</span><span class="sr-only">关闭</span>
</button>
<h5 class="modal-title" id="configTitle" data-lang="">增加/修改应用版本</h5>
<input type="hidden" id="versionId" >
<input type="hidden" id="appTypeId" >
</div>
<div class="modal-body">
<div class="row">
<div class="col-sm-6">
<div class="form-group">
<label>对外版本号</label>
<input type="text" id="versionName" class="form-control" placeholder="下载时显示的apk名称,无需加.apk后缀">
</div>
<div class="form-group">
<label>对内版本号</label>
<input type="text" id="versionCode" class="form-control">
</div>
<div class="form-group">
<label id="appfile_title">应用文件</label>
<div id="file-pretty">
<div class="form-group">
<input type="file" name="accountFile" id="appfile" class="form-control" >
</div>
</div>
</div>
<div class="form-group">
<label>发布版本</label>
<div class="checkbox checkbox-success">
<input id="checkbox2" type="checkbox">
<label for="checkbox2">
勾选并保存修改之后,当前版本将发布成博客原创Captain&D在线可用的最新版本
</label>
</div>
</div>
<div class="form-group">
<label>是否强制升级</label>
<div class="checkbox checkbox-success">
<input id="checkbox4" type="checkbox">
<label for="checkbox4">
当前版本启用之后,用户打开客户端后会立即强制升级成博客原创Captain&D当前版本
</label>
</div>
</div>
</div>
<div class="col-sm-6">
<div class="form-group">
<label>升级日志</label>
<textarea class="form-control" id="upgradeLog" rows="12" style="resize: none"></textarea>
</div>
</div>
</div>
<div class="row">
<p style="color:red;display: none" id="errMsg">
</p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-success" id="saveEdit" >保存</button>
<button type="button" class="btn btn-white" data-dismiss="modal" data-lang="close">关闭</button>
</div>
</div>
</div>
</div>
2、从页面上传附件相关处理方式(展开以显示代码)
<!-- Captain&D -->
<!-- https://www.cnblogs.com/captainad/ -->
$("#saveEdit").click(function () {
if(validateParam()) return; // 先进行存在性校验
var formdate = new FormData();
formdate.append('id', $("#versionId").val());
formdate.append('versionName', $("#versionName").val());
formdate.append('versionCode', $("#versionCode").val());
$('#loading-modal').modal("show");
$.ajax({
url: "versionmng/existsSameAppVersion",
type: "post",
data: formdate,
processData : false,
contentType : false,
success: function(data1){
if(data1.code == 200) { // 正式发起保存请求
var checked = $("#checkbox2").is(':checked');
var checked1 = $("#checkbox4").is(':checked');
var formdate = new FormData();
var fils = $("#appfile").get(0).files[0];
console.log(fils);
formdate.append('appFile', fils);
formdate.append('id', $("#versionId").val());
formdate.append('appType', $("#appTypeId").val());
formdate.append('versionName', $("#versionName").val());
formdate.append('versionCode', $("#versionCode").val());
formdate.append('upgradeLog', $("#upgradeLog").val());
formdate.append('appStatus', checked ? 1 : 0);
formdate.append('forcedUpgrade', checked1 ? 1 : 0); $.ajax({
url: "versionmng/addAppVersion",
type: "post",
data: formdate,
processData : false,
contentType : false,
success: function(data){
if(data.code == 200) {
$("#myModal_editApp").modal("hide");
$("#errMsg").html("");
$("#errMsg").css("display", "none");
swal("Successfully", "新增/修改App应用版本信息博客原创Captain&D成功", "success");
initload(pageObj);
}else {
swal("Failed", data.msg, "error");
}
$('#loading-modal').modal("hide");
}
}); }else {
swal("Failed", data1.msg, "error");
$('#loading-modal').modal("hide");
}
}
});
})
3、Captainad通过上传资源到云服务器的方法(展开以显示代码)
/**
* 增加应用版本
* Captain&D
* https://www.cnblogs.com/captainad/
*/
public Result addAppVersion(HttpServletRequest request, @RequestParam(value = "appFile", required = false) MultipartFile file) { ··· // 文件处理
if(file != null && file.getSize() > 0) {
// 检查文件类型
String filename = file.getOriginalFilename();
String suffix = filename.substring(filename.lastIndexOf("."), filename.length());
log.info("file format: {} {}", filename, suffix);
if ("1".equals(appType) && !".apk".contains(suffix) || "2".equals(appType) && !".ipa".contains(suffix)) {
return Result.builder()
.code(ResultLanguage.getResultMessage(ResultMessage.WRONG_APP_FORMAT).getCode())
.msg(ResultLanguage.getResultMessage(ResultMessage.WRONG_APP_FORMAT).getMsg()).build();
}
String appName = "";
if("1".equals(appType)) {
appName = versionName.replace(" ", "_").replace(".apk", "").concat(".apk");
}else {
appName = versionName.replace(" ", "_").replace(".ipa", "").concat(".ipa");
} try{
Map<String, String> fileMap = fileOperationService.uploadFile(appName, "/captainad/app/", file.getInputStream());
if(null != fileMap && !fileMap.isEmpty()) {
for(Map.Entry<String, String> set : fileMap.entrySet()) {
String downloadUrl = set.getKey();
String appMd5 = set.getValue();
requestMap.put("downloadUrl", new String[]{downloadUrl});
requestMap.put("appMd5", new String[]{appMd5});
}
}else {
return Result.builder()
.code(ResultLanguage.getResultMessage(ResultMessage.APP_UPLOAD_ERROR).getCode())
.msg(ResultLanguage.getResultMessage(ResultMessage.APP_UPLOAD_ERROR).getMsg()).build();
}
}catch (Exception e) {
log.error("上传客户端App文件存在异常。", e);
}
}
}
4、通过拼接字符串生成plist文件
/**
* 生成iOS应用对应的plist文件
* Captain&D
* https://www.cnblogs.com/captainad/
*/
private String genIosPlist(CaptainadAppVersionInfo captainadAppVersionInfo){
StringBuilder builder = new StringBuilder();
builder.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
builder.append("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
builder.append("<plist version=\"1.0\">");
builder.append("<dict>");
builder.append(" <key>items</key>");
builder.append(" <array>");
builder.append(" <dict>");
builder.append(" <key>assets</key>");
builder.append(" <array>");
builder.append(" <dict>");
builder.append(" <key>kind</key>");
builder.append(" <string>software-package</string>");
builder.append(" <key>url</key>");
builder.append(" <string>").append(captainadAppVersionInfo.getDownloadUrl()).append("</string>");
builder.append(" </dict>");
builder.append(" </array>");
builder.append(" <key>metadata</key>");
builder.append(" <dict>");
builder.append(" <key>bundle-identifier</key>");
builder.append(" <string>").append(getSetCacheService.getConfigValue("ios_bundle_identifier")).append("</string>");
builder.append(" <key>bundle-version</key>");
builder.append(" <string>").append(captainadAppVersionInfo.getVersionCode()).append("</string>");
builder.append(" <key>kind</key>");
builder.append(" <string>software</string>");
builder.append(" <key>title</key>");
builder.append(" <string>Captainad App</string>");
builder.append(" </dict>");
builder.append(" </dict>");
builder.append(" </array>");
builder.append("</dict>");
builder.append("</plist>");
String plistName = captainadAppVersionInfo.getVersionName().concat(".plist");
try {
InputStream is = new ByteArrayInputStream(builder.toString().getBytes("UTF-8"));
Map<String, String> fileMap = fileOperationService.uploadFile(plistName, "/captainad/app/plist/", is);
if(null != fileMap && !fileMap.isEmpty()) {
for(Map.Entry<String, String> entry : fileMap.entrySet()) {
log.info("生成的plist的文件地址:{}", entry.getKey());
return entry.getKey();
}
}
} catch (Exception e) {
log.error("生成plist文件时出现异常。", e);
}
return null;
}
5、数据库表设计(展开以显示代码)
-- Captain&D
-- https://www.cnblogs.com/captainad/
CREATE TABLE `captainad_app_version_info` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`version_name` varchar(64) DEFAULT NULL COMMENT '外部版本号',
`version_code` varchar(64) DEFAULT NULL COMMENT '内部版本号',
`upgrade_log` text COMMENT '更新日志',
`download_url` varchar(128) DEFAULT NULL COMMENT '版本路径',
`app_md5` varchar(32) DEFAULT NULL COMMENT '文件MD5',
`app_status` int(11) DEFAULT NULL COMMENT '版本状态(0-关闭,1-启用)',
`release_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
`forced_upgrade` int(11) DEFAULT '' COMMENT '是否强制升级(0-否,1-是)',
`app_type` int(11) DEFAULT NULL COMMENT '应用类型(1-Android,2-iOS)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='App版本管理';
6、safari通过访问路径之后的路由处理(展开以显示代码)
/**
* 进入App下载安装页面
* Captain&D
* https://www.cnblogs.com/captainad/
*/
@AuthorityVerify
@RequestMapping("ios")
public String toDownloadIosAppPage(HttpServletRequest request) {
String version = request.getParameter("version");
String httpsHost = getSetCacheService.getConfigValue("file_cloud_visit_host_https");
String plistUrl = httpsHost.concat("/captainad/app/plist/").concat(version).concat(".plist");
request.setAttribute("plist", plistUrl);
return "/appmng/ios_app";
}
7、应用下载页面的plist路由协议写法
<!-- Captain&D -->
<!-- https://www.cnblogs.com/captainad/ -->
<!-- 下载安装in-house应用关键代码 -->
<div class="wrapper wrapper-content">
<div class="row">
<div class="col-sm-12">
<div class="middle-box text-center animated fadeInRightBig" style="margin-top: 90%;">
<!--<h3 class="font-bold">这里是页面内容</h3>--> <div class="install-btn">
<br/><a href="itms-services://?action=download-manifest&url=${plist}" class="btn btn-success btn-lg m-t">
<i class="fa fa-apple"></i> Install Tesla app for iOS</a>
</div>
</div>
</div>
</div>
</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
苹果手机通过Safari浏览器访问web方式安装In-House应用的更多相关文章
- iphone手机safari浏览器访问网站滚动条不显示问题解决办法
近排有公司同事出差在外需使用OA系统,发现iphone手机safari浏览器在该出现滚动条的页面没有显示滚动条,导致无法正常使用. 系统前端页面是采用jeasyui搭建的框架,使用iframe变更主页 ...
- 解决无法通过浏览器访问docker成功安装rabbitMQ后页面问题
我发现了问题后,花了两天的时间解决了这个问题. 一.测试在docker本机中使用curl "ip地址:端口" 查看是否能访问成功,结果是没问题,排除了docker安装失败的问题 二 ...
- 浏览器访问web站点原理图
启动tomcat,在浏览器中输入http://localhost:8080/web_kevin/hello.html,发生的事情如下: 1.浏览器解析主机名,即解析localhost.浏览器首先会到本 ...
- 首次远程安装 GlassFish 后以远程 Web 方式访问其后台管理系统出现错误的解决方法(修订)
首次远程安装 GlassFish 服务后,如果以远程 Web 方式访问其后台管理系统,会提示 Secure Admin must be enabled to access the DAS remote ...
- [原创]java WEB学习笔记55:Struts2学习之路---详解struts2 中 Action,如何访问web 资源,解耦方式(使用 ActionContext,实现 XxxAware 接口),耦合方式(通过ServletActionContext,通过实现 ServletRequestAware, ServletContextAware 等接口的方式)
本博客的目的:①总结自己的学习过程,相当于学习笔记 ②将自己的经验分享给大家,相互学习,互相交流,不可商用 内容难免出现问题,欢迎指正,交流,探讨,可以留言,也可以通过以下方式联系. 本人互联网技术爱 ...
- chrome浏览器如何在本地安装谷歌访问助手教程
许多用户都需要使用谷歌的gmail,搜索.我们目前可以用谷歌访问助手解决google无法访问的问题.那么谷歌访问助手在chrome浏览器中如何安装和使用呢?今天我们详细介绍. 本地安装谷歌访问助手的步 ...
- Struts2中访问web元素的四种方式
Struts2中访问web元素的四种方式如下: 通过ActionContext来访问Map类型的request.session.application对象. 通过实现RequestAware.Sess ...
- 解决java web中safari浏览器下载后文件中文乱码问题
解决java web中safari浏览器下载后文件中文乱码问题 String fileName = "测试文件.doc"; String userAgent = request.g ...
- vbox安装增强功能,实现宿主机文件夹共享并浏览器访问
虚拟机版本:6.0.4 r128413 (Qt5.6.2) linux:centos7/6 点击菜单栏中的设备->安装增强功能,再reboot 获取内核版本号 uname -r 查看yum的内核 ...
随机推荐
- JSP 用poi 读取Excel
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding= ...
- MySQL 权限生效
用GRANT.REVOKE或SET PASSWORD对授权表施行的修改会立即被服务器注意到. 如果你手工地修改授权表(使用INSERT.UPDATE等等),你应该执行一个FLUSH PRIVILEGE ...
- 设置label的字体
label.font = [UIFont fontWithName:@"Arial-BoldItalicMT" size:24]; 字体名如下: Font Family: Amer ...
- js程序开发-1
<h1>数组的常用操作</h1> push() 方法可向数组的末尾添加一个或多个元素,并返回新数组的长度. unshift() 方法可向数组的开头添加一个或更多元素,并返回新数 ...
- javascript 树形菜单
1. [代码][JavaScript]代码 var ME={ini:{i:true,d:{},d1:{},h:0,h1:0,h2:0},html:function(da,f){var s='& ...
- 平衡二叉树、B树、B+树、B*树、LSM树简介
平衡二叉树是基于分治思想采用二分法的策略提高数据查找速度的二叉树结构.非叶子结点最多只能有两个子结点,且左边子结点点小于当前结点值,右边子结点大于当前结点树,并且为保证查询性能增增删结点时要保证左右两 ...
- Com组件介绍
COM组件简介 面向对象的思想难以适应这种分布式软件模型,于是组件化程序设计思想得到了迅速的发展. 按照组件化的程序设计的思想,复杂的应用程序被设计成一些小的,功能单一的组件模块,这些组件模块可以运行 ...
- nginx下laravel框架rewrite的设置
nginx下laravel框架rewrite的设置 百牛信息技术bainiu.ltd整理发布于博客园 在nginx的vhost站点配置文件中加入以下内容即可 1 2 3 4 5 6 7 8 9 10 ...
- ccflow汇总帖
视频教程学习 公司电脑路径; E:\开源工作流\ccflow佳怡物流版\ccflow\doc cclfow的码云地址: https://gitee.com/opencc/ccflow 在线demo演示 ...
- liteos内存(三)
1. 概述 1.1 基本概念 内存管理模块管理系统的内存资源,它是操作系统的核心模块之一.主要包括内存的初始化.分配以及释放. 在系统运行过程中,内存管理模块通过对内存的申请/释放操作,来管理用户和O ...