Hybrid混合开发学习笔记(1)混合应用开发定义和常见问题

一、什么是混合应用

混合应用是指同时使用前端技术与原生技术开发的 App。通常由前端负责大部分界面开发和业务逻辑,原生负责封装原生功能供前端调用,二者以 WebView 作为媒介建立通信,从而既拥有 Web 开发的速度优势,又能拥有强大的原生能力。

混合应用框架的本质就是上面提到的那个原生 App 外壳,这个外壳重点实现三件事:

  1. 实现原生与前端(Javascript)的交互;
  2. 封装基本的原生功能,供前端调用;
  3. 实现原生插件机制,供原生开发者扩展功能。

只要做到这三件事,基本上就可以被称为混合开发通用框架。

混合应用开发平台实际上已经将 Hybrid App 开发完全变成了前端开发者“一个人的事”,普通原生需求都内置了,扩展原生需求可以借助插件生态实现,前端转场不流畅可以用原生效果代替,甚至还提供 App 开发的全生命周期管理功能,包括 App 配置、项目管理、更新、统计等。可以说,混合应用开发平台这种模式的出现,将 Hybrid App 开发的技术门槛降到了最低,真正实现了只要一名前端就能开发跨平台 App 的目标,而且整个开发过程只需要用到 Web 前端技术,因此开发速度可以非常块,一个熟练的前端开发者

优缺点

混合开发方式可以在只投入一名前端开发者的情况下,快速开发出兼容多个平台的 App,相比原生开发同时降低了开发的时间成本和人力成本,这是混合开发能够一直维持旺盛生命力的根源。

但有得必有失,我们也必须正视混合开发的弊端,受限于 HTML5 的表现力,混合应用在 UI 层面很难达到原生界面的细腻程度;界面的载入速度也很容易受到手机运行速度和页面大小的影响。如果前端开发做的不够细致,就很容易给用户带来“网页感”,使 App 的用户体验大打折扣。

 结合上述优缺点分析,混合开发方式比较适合以下类型的项目:

扫描二维码关注公众号,回复: 9750641 查看本文章
  • 功能导向的项目,例如企业内部 App、面向特定用户的工具类 App;
  • 需要快速开发迭代的项目,例如新产品试水、外包项目;
  • 缺少原生开发团队的企业。

完全可以在几天之内完成一个中小规模的 App 开发。

二、 混合应用开发 常见问题

从技术上讲,混合应用界面和移动端网页并无二致。可如果将移动端网页原样放在混合应用里运行,界面效果就大大折扣了。仔细观察会发现,相比原生界面,普通网页在很多细节呈现上做得不够细腻,造成这种结果的主要原因有两点,

  1. 前端没有正确适配屏幕尺寸,导致布局走样或大小间距不当,从而影响整体观感;
  2. 网页渲染对硬件适配能力先天不足,容易出现模糊现象,导致界面不精细。

1、页面适配

移动端页面适配是前端开发领域的老话题了。随着系统和设备的更新迭代,目前只要在页面头部设置 viewport 适配代码,再配合 flex 弹性布局,基本上可以满足大部分的适配需求。

//viewport适配代码
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">

但上述方式只能实现相对“粗略”的适配,即便设置了 viewport,不同手机间的像素差异仍然很大。市面上常见尺寸中,宽度最小的手机只有 320px,最大的 iPhone 8 Plus 却达到了 414px,这种情况下想用 px 实现精细布局已不太可能。我们需要一种可以随屏幕宽度变化而变化的相对单位,才能做到真正的适配。目前,rem 方案的可靠性和兼容性已经得到业内的广泛认可,这里我们也推荐一种非常简单的基于 rem 的适配方案。

适配原理是先假定我们的屏幕宽度为 640px,此时将根节点(html)字号设置为 10px,即 “1rem=10px”,进而得到屏幕总宽度等于 64rem。

html{font-size:10px;}

/* 此时屏幕宽度 = 64rem */

当以此标准实现的页面运行在其他设备上时,我们只要通过改变根节点的字号,使“屏幕总宽度=64rem”这个等式始终成立就可以了,之前的做法是 JavaScript 检测屏幕宽度,然后计算出 rem 的值。

var html=document.documentElement;
var rootSize = html.clientWidth / 640 * 10;
html.style.fontSize=rootSize+'px' 

随着规范的推广和手机系统的更新,如今我们用 CSS 也能实现相同效果,因为新规范中有一个 vw 单位,能够以屏幕宽度为单位1,实现任意百分比例的取值,即:

屏幕宽度=100vw

关联我们需要实现的等式屏幕宽度 = 64rem,可以得到:

100vw = 64rem

进而得到:

1rem = 1.5625vw

实际上这样就实现了屏幕总宽度恒等于 64 rem,然后就可以愉快的使用 rem 单位做可以适配任何屏幕的精确布局了。

以上是理论,实际上这里会产生一个 Bug。由于多数屏幕的宽度都小于 640px,计算出来的 rem 也会小于 10px,但 WebKit 内核会强制将最小字号锁定在 12px,这将直接导致我们的适配等式无法成立,从而在应用中出现较大的偏差,所以实际开发中我们要将根节点(html)字号矫正为 15.625vw ,适配等式就变成了“屏幕总宽度=6.4rem”,那么“1rem=100px”,可以绕过 WebKit 的限制,同时开发中的换算压力也不大。

说了这么多,真正需要写的代码只有一行:

html { font-size: 15.625vw;}

此时在 640px 宽度的页面中,1rem=100px,且无论在任何尺寸的手机上都会保持这个比例,简单的应用示例如下:

<div style="width: 3.2rem;height:3.2rem;background:black;">
  在任何手机屏幕上都显示为屏幕宽度50%的正方形
</div>

前端基于这套适配方案开发,需要将设计稿宽度约定为 640px,当在设计稿上量取20px时,代码中只要除以100,就可以很简单的换算得到 0.2rem。

2、精细还原

提升网页的精细程度可以从两方面入手,一是使用技术手段排除显示层面的模糊现象,二是用心还原设计意图。

显示模糊

常见的显示模糊有两种情况,一是图片模糊,二是边框模糊,有经验的前端开发者应该对这两个问题都不陌生。

图片模糊的原因是手机屏幕的可测量尺寸与物理像素尺寸不一致,通常 Web 前端会习惯性的将图片尺寸切成可测量尺寸,而图片显示最清晰的状态应该是图片尺寸与显示屏的物理像素尺寸一致的时候

以红米4的屏幕为例,添加 viewport 适配代码以后,屏幕的可测量宽度为 360px,但这块屏幕的物理像素宽度却是 1080px,说明这块屏幕的像素比(DPR)是3,也就是说显示的时候会用3个物理像素去模拟一个像素,来提高屏幕的显示精度。

如果在这块屏幕上显示图片,理论上一张宽度360px的图片已经可以自动全屏了,但由于图片的像素数过低,包含的信息量不够分配给每一个物理像素,显示的时候就会通过插值算法生成更多的像素,去分配给物理像素显示,这必定会导致图片的显示锐度下降。而如果图片本身宽度就是 1080px 的话,所包含的像素信息就正好能够分配给每一个物理像素,此时便可在这块屏幕上呈现最佳的显示效果。

也不是图片只要够大就没问题,图片尺寸大了将直接影响加载速度和内存占用,所以还要根据实际情况做取舍。如果是 App 的界面素材,通常会随 Hybrid App 打包进本地,这时不需要考虑加载速度,可以适当增大图片尺寸,目前主流机型的屏幕宽度最大就是 1080px,切图时可以参考这个值取适当大小的切片。而如果是业务中的远程图片,考虑到加载速度,单张图片大小应控制在 50k 以内,如果后端能自动压缩图片最好,否则就只能控制图片的尺寸了。

个别情况也可以例外,例如产品详细页,通常只有几张产品大图,并发不会太多,而且通常会做成轮显效果,第一时间只会显示第一张图片,这些因素就为图片加载创造了很好的条件。因此为了保证显示效果,将产品图做大一点也没有关系。

但如果是带缩略图的产品列表页,就一定要严格控制缩略图尺寸了,原因有两点,产品列表首屏显示6-10个产品很正常,这就是6-10个图片并发,图片加载慢的话很容易让加载时间超过1s,影响用户体验。另外产品列表通常会做成滚动加载,随着用户浏览加载的图片越来越多,手机的内存占用也会急剧上升,App的运行会更耗电。

总而言之,本地图片尽量做大,远程图片根据需求和场景做适当取舍,不能只为了显示清晰而丢了加载速度。

边框模糊也就是经典的 1px 边框问题,其产生的原因,从本质上来讲,跟图片模糊的原因一样。用 CSS 画出的 1px 只是可测量尺寸上的 1像素,不能保证就是物理层面上的1像素。在视网膜屏成为标配的今天,CSS 画的 1px 边框基本上都会被呈现为物理像素 2px 或 3px,到了界面上就会显得不精细,跟原生显示的真正1像素有明显差异。

1px 问题的解法有很多,其中利用 transform 所实现的方法应该是最方便的解法了。代码如下所示:

.border-bottom{border-bottom:1px solid #ccc;}

@media screen and (-webkit-min-device-pixel-ratio: 2) { 
  .border-bottom{position:relative;border:0;}
  .border-bottom:before{
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 1px;
    content: '';
    transform: scaleY(.5);
    background-color: #ccc;
  }
}

这样显示出来的是真正的1像素边框,看上去非常锐利,显得界面更精致。

还原设计

还原设计这个点其实没有太多可说的,作为前端开发理应忠实的还原设计。但根据我的经验,PC 时代还原设计还好说,毕竟直接量设计稿就可以达到像素级别的精度,但在 Hybrid App 开发中,压根就不存在什么像素级还原。因为最终的界面要跑在各种不同尺寸的屏幕上,这个界面是没有标准答案的,我们前端能做的只能是在所有屏幕上都尽可能的还原“设计意图”,不至于让界面看上去跟设计稿不是一回事。

这里我简单提一下我认为比较容易出问题的两个点。

首先是字号设置。我们使用的 rem 适配方案,理论上只要所有单位都用 rem 实现,是可以将设计稿完美适配到所有屏幕的,但文字是界面上比较特殊的一类元素,它们的适配逻辑并不应该简单的仿效布局适配。对于阅读性文字理应设置成一个最适合阅读的固定尺寸(px),大屏就显示的多一些,小屏就显示的少一些,而不是随着屏幕宽度增加而等比增大字号;修饰性文字则完全可以使用 em 单位做相对缩放,这样从逻辑上更能体现其与父元素之间的关系,而不是强调其与屏幕宽度之间的关系,这一点当手机横屏显示时差异非常大,使用 rem 单位的文字在横屏下会大的离谱,甚至会直接导致布局失效。

所以我通常还是会用 px 或者 em 单位进行字号设置。但这样做也存在问题,就是只要稍微设置不当,就显得文字与周围布局不协调,从而破坏设计意图。这时候我会反复调整字号大小,并对比观察界面和设计稿,直到它们看上去感觉一样为止。

/*阅读类文本使用固定像素*/
.p{font-size:14px;}

/*标题类文本使用相对字号*/
.pro_title{font-size:1.2em;}
.channel_title{font-size:1.8em;}

除了字号以外,有些间距设置有时也不应该使用 rem 单位,究其根本还是因为它们从内在逻辑上就跟屏幕宽度没关系,比如说下图文字列表的 padding:

item

这个间距从设计意图上说,是列表项文字的“呼吸空间”,它的大小应该只跟文字大小有关系,凡是这种地方,都没有办法“像素级”还原设计稿,我们只能用心调试,去忠实的还原设计意图。

/*使用em单位设置文字间距*/
.item{...;padding: 1.2em;}

有时候页面四周与内容之间也会有一个间距,这种间距我们可理解为布局的延申,因此应该和布局一样使用 rem 单位实现,比如下面这种页面四周的间距:

.wrap{...;padding:0.8rem;}

总结下来,凡是跟布局无关的地方,其实都不太适合用 rem 单位,这些地方都需要我们认真对待,避免失真,只有所有细节都做好了,才能整体呈现出较高的还原度。

3、页面加载优化

Hybrid App 会将页面打包到本地,资源加载问题虽然得到了根本上的缓解,但不代表我们就可以完全无视加载问题。实际上以当今手机的平均性能水平来看,如果页面的 CSS 和脚本体积过大,仍然可能造成肉眼可见的渲染延迟,这在 App 中是不能容忍的。

造成渲染延迟的原因,第一是由于静态资源读取慢,第二是手机的运算能力普遍不足,导致页面绘制时间和脚本执行时间过长,所以 Hybrid App 开发中我们仍然要对页面性能严格要求,务必保证页面打开后立即呈现。关于代码压缩合并、CSS 性能、JS 性能等问题属于前端基础知识,这里不再赘述。

数据加载优化

App 界面除了页面自身结构外,往往还有很多内容是由异步数据渲染出来的。对于这些业务数据,我们没办法要求“立即呈现”,在等待数据的过程中,我们可以从三个方面着手提升体验,首先是让页面框架先加载,内容区域填充占位内容,给用户造成立即呈现的错觉;其次要显示生动且合理的加载动画,缓解用户的等待焦虑;再次从数据源着手提速,可以考虑加带宽,或者减少单次请求数据量等方式,给接口提速,毕竟这才是一切的根源。

第一点也就是 PWA 中 App Shell 的概念,也叫做骨架屏,即实现一个只包含布局骨架的页面,每次先加载,内容区域可以填充一些占位元素,等待异步数据就绪再填充真实内容,如下图所示:

骨架屏的重点是占位元素的实现,大致分两种方式。

一种是手写占位元素,然后用真实内容的 DOM 元素直接替换,例如:

<style>
.shell .placeholder-block {
    display: block;
    height: 5em;
    background: #ddd;
    margin: 1em;
}
</style>
<div class="shell">
    <div class="placeholder-block"></div>
    <div class="placeholder-block"></div>
</div>

效果如下图所示:

这种方式比较简单直接,但缺点是占位元素的样式需要单独维护,如果整个项目涉及多处不同的占位元素,工作量就比较大了。

另一种思路是利用真实内容的 DOM 结构和样式占位。占位状态下叠加一个样式用来呈现 loading 状态,我们看下面这段代码:

<style>
.list{overflow:hidden;}
.list .avat{float: left;width:5em;height: 5em;overflow:hidden;border-radius: 2.5em;margin-right: 1em;}
.list .title{height: 5em;overflow:hidden;}
/*占位元素通用类*/
.placeholder .avat {
    background: #ddd;
}
.placeholder .title{
    background: #ddd;
}
</style>
<div class="list placeholder">
    <div class="avat"></div>
    <div class="title"></div>
</div>

效果如下图所示:

当真实内容渲染后,需要将占位状态的叠加类去掉。

图片加载优化

图片是最能拖慢页面显示速度的元素,在图片扎堆的列表页上这个问题最为严重,如果有必要我们也可以采用 Web 中常用的图片懒加载技术,但懒加载的本质是分散加载时间,提升首屏展示速度,资源总加载时间并没有减少,所以如果首屏显示不是特别迟滞,使用懒加载的意义并不大。给图片提速最好的解决方式是缓存,有条件可以使用 CDN 加速,没条件可以在 App 端做图片缓存,但这需要结合原生能力,我们将在后面的实战章节中具体讲到。

吸引注意力

如果各种技术手段都上了,仍然觉得不够“快”,那么还有最后一个体验优化思路,就是当用户操作时给以积极的界面反馈,从而吸引用户注意力,避免等待焦虑,比如给按钮做一个绚丽的点击动画、给控件做一个漂亮的过渡动画,都属于这一类优化方式,说白了就是障眼法,但效果不错。

波纹动画

我们看下上图波纹动画的实现过程。

约定触发波纹动画的元素都必须带有 active 属性,且相对或绝对定位,元素超出则隐藏。波纹元素采用绝对定位,我们如下撰写代码:

<div active style="position:relative;overflow:hidden;border:1px solid #ccc;padding:1em;">
    波纹动画
</div>

CSS 部分需要实现波纹元素的初始状态和动画状态:

.active-handle{
  position: absolute;
  width:400px;
  height: 400px;
  border-radius: 200px;
  background: #dedede;
  z-index: 0;
  transform:scale(0);
  -webkit-transform:scale(0);
  opacity: .5;
  transition:all ease-out .5s;
  -webkit-transition:all ease-out .5s;
}
.active .active-handle{transform:scale(1);-webkit-transform:scale(1);opacity: 0;}

JS 部分为目标元素绑定触摸事件,当触发 touchstart 事件时,在元素内生成波纹元素并展示 CSS 动画:

var $body = $('body');
//批量绑定active事件
$body.on('touchstart', '[active]', function(e) {
    var target = e.target;
    target.classList.remove('active');
    var activeHandle = document.createElement('div');
    activeHandle.classList.add('active-handle');
    var targetOffset = e.touches && e.touches.length ? e.touches[0] : e.touches;
    var eleOffset = target.getBoundingClientRect();
    if(targetOffset && eleOffset){
        activeHandle.style.left = targetOffset.clientX - eleOffset.left - 200 + 'px';
        activeHandle.style.top = targetOffset.clientY - eleOffset.top - 200 + 'px';
        target.normalize();
        var lastNode = target.lastChild;
        if(lastNode){
            if(lastNode.nodeName==='#text' && !lastNode.nodeValue.trim()){
                lastNode = lastNode.previousSibling;
            }
            target.insertBefore(activeHandle, lastNode);
        }else{
            target.appendChild(activeHandle);
        }
        setTimeout(function(){
            target.classList.add('active');
        },0);
    }

    target.setAttribute('data-touch', 1);

}).on('touchcancel', '[active]', function(e) {
    var target = e.target;
    target.classList.remove('active');
    target.removeAttribute('data-touch');
}).on('touchmove', '[active]', function(e) {
    var target = e.target;
    target.classList.remove('active');
    target.removeAttribute('data-touch');
}).on('touchend', '[active]', function(e) {
    var target = e.target;
    var oldNode = target.querySelector('.active-handle');
    setTimeout(function(){
        if(oldNode){
            target.removeChild(oldNode);
        }
        target.classList.remove('active');
    },500)
});

4、App 优化

App 开发与传统 Web 前端开发有几点明显的不同,比如运行时需要监听网络状况,并对异常情况做处理;需要自带更新机制,因为 App 本身无法“刷新”……总而言之一句话,App 作为一个独立客户端,需要自己管理好自己的“生老病死”,这是刚接触 Hybrid App 开发的前端同学需要学习并适应的一件事。

网络状态管理

网络状态管理,包括请求异常处理和网络异常处理。当请求出现异常时,我们需要捕获异常,使界面呈现恰当的内容,比如在内容区域填充异常提示界面,或者给用户恰当的弹窗或气泡提示。这需要我们的异步请求方法能够集中处理异常,以 HybridStart 框架的异常处理为例:

//ajax错误处理
catchAjaxError = function(code, status) {
    switch (code) {
        case 0:
            app.toast('网络错误,请检查网络连接!' + status);
            break;
        case 1:
            app.toast('请求超时!');
            break;
        case 2:
            app.toast('授权错误!');
            break;
        case 3:
            app.toast('服务端数据异常!');
            break;
        default:
            app.toast('服务端错误(' + status + ') code:' + code);
    }
};

当监听到设备离线时,所有的异步数据都将不可用,此时 App 将无法正常提供服务,此时我们可以让 App 跳转到一个异常页面以提示用户:

断网提示

以 APIcloud 为例,假如断网页面的 URL 是 ./error/offline.html,当监听到设备离线时,可以直接跳转到该页面:

api.addEventListener({
    name:'offline'
}, function(ret, err){        
    api.openWin({
        name: 'offline',
        url: './error/offline.html'
    });
});

断网是极端情况,但为了尽可能保证断网后 App 的使用体验,我们最好能将已访问过的数据缓存起来,断网时可以继续为用户提供缓存数据,当然这个要根据业务特点决定是否适合。

沉浸式状态栏

随着全面屏在手机工业设计上的流行,软件层面与之配合的沉浸式状态栏也逐渐成为了手机系统的标配,开启沉浸式特性的确会使 App 的界面设计呈现出更好的整体效果。这种系统层面的优化手段,使用成本低,产出效果好,我们应该积极响应。

沉浸式状态栏

这种特性通常只需要一个简单的配置就可以实现。仍然以 APICloud 为例,我们只需要在配置文件中添加一行代码即可:

<preference name="statusBarAppearance" value="true" />

适当引入原生能力

当有些需求超出 Web 前端能力的时候,我们只能借助原生能力实现。比如 App 开发经常会遇到的输入框需求,希望新页面打开后输入框立即获取焦点,并自动弹出软键盘,这个功能使用纯前端能力无法完美实现,如果这个需求对产品确实非常重要,那么就只能使用原生 input 控件来实现。

如何引入原生能力取决于开发采用的框架,通常的混合应用开发框架都有自己的原生插件生态,可以从插件库里找到自己需要的插件。

4、防攻击

客户端最容易成为 XSS 攻击目标。防止 XSS 需要从内容的输入和输出两方面做过滤,当 Hybrid App 作为客户端时,有可能需要显示一些异步获取的 HTML 片段,此时其作为潜在 XSS 攻击代码的输出端,就需要做好 XSS 过滤。事实上,所有的WEB页面只要涉及到异步内容渲染,都应该对内容做XSS过滤。

这里推荐 jsxss ,它是专做前端 XSS 过滤的库,使用非常简单,示例如下。

<script src="https://raw.github.com/leizongmin/js-xss/master/dist/xss.js"></script>

var html = filterXSS('<script>alert("xss");</scr' + 'ipt>');
alert(html);

如果 App 中需要展示异步获取的 HTML 片段,拿到数据后应该先用 filterXSS 方法过滤一遍再填充到页面中:

asyncCallback(htmlData){
    var $view = document.getElementById('view');
    var cleanHTML = filterXSS(htmlData);
    $view.innerHTML = cleanHTML;
}

这个库的配置功能非常强大,基本上可以满足任何定制需求,配置项参见这里

防 XSS 的关键在于不信任用户的任何输入,输出端过滤主要用来抵御反射型 XSS,还有一种危害更大的持久型 XSS,需要在输入端(也就是服务端)做好内容过滤。

5、防代码泄露

混合应用的(前端)代码非常容易泄露,只要将安装包后缀名改成 .rar,然后用解压工具打开很容易就能找到所有的前端代码。

混合应用代码泄露

项目架构正常的情况下,前端代码的业务安全价值应该不大,但有时候我们可能出于防抄袭等目的,希望对前端代码加密。

传统 Web 前端的 JS 加密/混淆方式仍然可以使用。JS 加密基本上足够挡住一批小白,但破解方法其实有很多,一旦被找到还原方法代码就会彻底暴露,安全等级很低;代码混淆是无法还原的,安全性相对高一些,但如果有人愿意肉眼强攻,强行解读代码,那破解就是时间问题。另外,代码混淆还会降低代码执行性能,对于计算能力本来就不强的移动端来说,这一点还是挺敏感的,项目中我们可以只对业务代码混淆,对于体积大、调用频率高的类库文件,最好还是不要混淆了,一来混淆价值不大,二来对性能的影响过大。

如果希望进一步提高加密等级,在 HybridApp 模式下可以结合原生能力实现其他的加密方式。

例如将前端代码打包到 App 里之前,先加密压缩成 Zip文件,App 启动后先解压得到代码,再执行前端页面。这种方式相当于将破解难度完全转嫁到了压缩包的破解上,实现简单,破解难度较高。

另一种方式是使用加密算法先将前端文件加密,然后打包进 App,在 App 外壳上用原生实现相应的解密算法,启动后先解密,再执行前端页面。这种方式的加密过程完全由我们自主控制,理论上可以得到非常好的加密效果,以 APICloud 平台的加密功能为例,加密后的前端代码是这样的:

加密

这种加密等级已经非常高了,基本可以做到不可逆。

以上两种依赖原生实现的加密方式,单纯从加密的角度可以说完全达到了目的,但思路本身存在致命漏洞,加密效果并不可靠

因为前端代码无论如何加密,最终都是要还原回来在 WebView 里运行,而在 Android 平台可以借助 Chrome Inspect 工具直接远程调试 WebView 里的页面,不但能看到解密后的代码,甚至可以打断点调试,效果完全跟在本地运行一样!这样的话,后两种加密方式的效果反而还不如前端混淆了。

Chrome inspect

这就应了一句话:安全问题是全链路问题,单一环节的安全性做的再高都没有用,整个系统的安全性取决于最薄弱的那个环节有多弱,而不是最强悍的那个环节有多强。说到底前端代码本来就是公开的,JS 加密/混淆已经是综合各种因素之后比较平衡的加密方案了,用来防抄袭已经足够,试图对前端文件进行强加密的做法,本身思路就不对,保护商业秘密的最好方法是,不要把它们放在前端。

:使用 Chrome Inspect 调试需要 App 的 WebView 开启 Debug 模式,但正式打包的 App 肯定不会开启 Debug 模式,所以一般情况下无法使用这个方法。但安卓系统 root 后,几乎没有不可能的事,网上可以找到教程,实现对安卓系统任意 App 的 WebView 内容进行远程调试。

6、防数据泄露

敏感数据加密

结合以上分析我们知道,无论怎么加密,前端代码都不存在绝对的安全,如果确实需要在 App 的前端代码中存放或使用敏感数据,我们只能综合使用多种加密方式,尽量提高破解难度,让攻击者知难而退。

首先我们可以将敏感数据加密保存在原生文件中,只为前端提供一个加密数据的获取方法:

// APICluod获取原生加密数据
var secretKey = api.loadSecureValue({
    sync: true,
    key: 'appKey'
});
alert(secretKey)

原生加密的数据很难破解,这样数据的保存环节可以认为是牢固的,剩下的风险就只在前端调用环节。如果攻击者使用 Chrome Inspect 调试 WebView,仍然可以通过断点调试得到想要的数据。这时我们可以将调用数据部分的 JS 逻辑加密/混淆,使其不可读,即使攻击者调试页面,难度也将大大提高,很难直接拿到敏感数据。

这套方案的安全隐患在于 JS 混淆的效果,理论上是不够可靠的,但对于安全性的评估,不应该只考虑防御方案是否绝对可靠,我们还要综合被保护对象的商业价值,决定将“安全城墙”堆到多高就可以,然后再从高度达标的方案中,选择实施成本最低的那一种。如果是极为重要的私密数据,放在前端肯定不安全,但在现有的安全等级前提下,什么可以放在前端什么不能放在前端,这是业务主导者需要考虑的。

数据通讯加密

App 的运行离不开与后端的数据通讯,一次完整的异步请求需要客户端发起、服务端返回、客户端接收,过程比较长,而且过程中还涉及到太多不可控的外部环境,例如路由器、运营商节点、CDN 服务器等等,任何一个环节都可能被利用发起攻击,因此通讯安全是系统安全的重中之重。

随着 HTTPS 的普及,中间人攻击和网络监听的风险已经大大降低,但因为 Hybrid App 存在整个客户端被破解的可能,攻击者可以从客户端直接拿到所有请求信息,因此如果希望保证项目的通讯安全,我们需要对整个请求过程进行加密,包括发送请求时的 URL 和参数,以及请求返回的数据。

加密请求的参数信息,可以防止攻击者猜测参数规律,滥用接口任意获取数据;加密接口返回的数据,能防止通信被已破解的客户端窃听,配合代码混淆,能一定程度上隐藏业务逻辑。

还有一种比较棘手的攻击方式:重放攻击,不需要解读数据,只通过将已经成功发起的请求进行重放,就可以巧妙的对系统实施攻击。重放攻击的防御难点在于,被重放的请求本质上是合法的,只不过发生的时机不对,因此针对重放攻击的防御手段都是围绕请求时机判定展开的,比如给请求加唯一标识,服务端通过判断标识重复辨别重放攻击;同理也可以使用时间戳,递增序号等方式,这些方式虽然有效,不过一旦被发现也很容易伪造,而将请求加密后就能隐藏请求的参数信息,虽然不能直接防御重放攻击,但可以隐藏防御手段,从而顺利实施防御措施。

实现请求加密,需要在客户端和服务端基于同一套加密算法分别实现加密、解密方法。客户端发起请求前,将请求参数加密后作为新的参数发送出去,服务端接收参数后解密得到实际参数,查询获得请求数据,然后将数据加密返回给前端,前端拿到数据后先解密,再传给业务逻辑使用。

请求加密

前端所需要做的就是封装一个公用异步请求方法,如上图中的大虚线框所示,这个方法除了正常发送 AJAX 请求外,最重要的是集中对 AJAX 请求参数加密,以及将 AJAX 返回的加密数据解密。加解密算法通常采用强加密算法,以 3DES 算法为例,加解密过程需要约定一个密钥,这个密钥前后端需要保持一致,对于 App 端来说,这个密钥一旦暴露就很可能导致整个加密方式被破解,因此我们必须使用前文提到的敏感数据加密手段进行保护。

公用加密请求方法的伪码如下:

var secretAjax = function(opt){
    // ajax配置
    // var opt = {
    //  url: AJAX_URL,                  //请求url
    //  data: AJAX_PARAM,               //请求参数
    //  success: AJAX_CALLBACK          //请求回掉
    // }

    // 3DES加密配置
    var cryptocfg = {
        mode: CryptoJS.mode.ECB,
        padding: CryptoJS.pad.Pkcs7
    };
    // 从原生接口取出加密密钥
    var secretKey = GET_SECRET; 
    var keyHex = CryptoJS.enc.Utf8.parse(secretKey);

    // 加密参数
    var paramDataStr = JSON.stringify(opt.data);
    var secureData = CryptoJS.TripleDES.encrypt(paramDataStr, keyHex, cryptocfg);
    var secureDataStr = secureData.ciphertext.toString();
    opt.data = {
        data: secureDataStr
    };


    var userCallback = opt.success;
    opt.success = function(res){
        // 解密数据
        var mi = $.trim(res);
        var encryptedHexStr = CryptoJS.enc.Hex.parse(mi);
        var encryptedBase64Str = CryptoJS.enc.Base64.stringify(encryptedHexStr);
        var decrypted = CryptoJS.TripleDES.decrypt({
            ciphertext: CryptoJS.enc.Base64.parse(encryptedBase64Str)
        }, keyHex, {
            mode: CryptoJS.mode.ECB,
            padding: CryptoJS.pad.Pkcs7
        });
        var ming = decrypted.toString(CryptoJS.enc.Utf8);
        // 将明文数据返给业务逻辑
        userCallback(JSON.parse(ming));
    }

    // 发送ajax
    sendAjax(opt)


}
发布了107 篇原创文章 · 获赞 178 · 访问量 87万+

猜你喜欢

转载自blog.csdn.net/bihansheng2010/article/details/100886979