Hybrid混合開發學習筆記(1)混合應用開發定義和常見問題

一、什麼是混合應用

混合應用是指同時使用前端技術與原生技術開發的 App。通常由前端負責大部分界面開發和業務邏輯,原生負責封裝原生功能供前端調用,二者以 WebView 作爲媒介建立通信,從而既擁有 Web 開發的速度優勢,又能擁有強大的原生能力。

混合應用框架的本質就是上面提到的那個原生 App 外殼,這個外殼重點實現三件事:

  1. 實現原生與前端(Javascript)的交互;
  2. 封裝基本的原生功能,供前端調用;
  3. 實現原生插件機制,供原生開發者擴展功能。

只要做到這三件事,基本上就可以被稱爲混合開發通用框架。

 

混合應用開發平臺實際上已經將 Hybrid App 開發完全變成了前端開發者“一個人的事”,普通原生需求都內置了,擴展原生需求可以藉助插件生態實現,前端轉場不流暢可以用原生效果代替,甚至還提供 App 開發的全生命週期管理功能,包括 App 配置、項目管理、更新、統計等。可以說,混合應用開發平臺這種模式的出現,將 Hybrid App 開發的技術門檻降到了最低,真正實現了只要一名前端就能開發跨平臺 App 的目標,而且整個開發過程只需要用到 Web 前端技術,因此開發速度可以非常塊,一個熟練的前端開發者

優缺點

混合開發方式可以在只投入一名前端開發者的情況下,快速開發出兼容多個平臺的 App,相比原生開發同時降低了開發的時間成本和人力成本,這是混合開發能夠一直維持旺盛生命力的根源。

但有得必有失,我們也必須正視混合開發的弊端,受限於 HTML5 的表現力,混合應用在 UI 層面很難達到原生界面的細膩程度;界面的載入速度也很容易受到手機運行速度和頁面大小的影響。如果前端開發做的不夠細緻,就很容易給用戶帶來“網頁感”,使 App 的用戶體驗大打折扣。

 結合上述優缺點分析,混合開發方式比較適合以下類型的項目:

  • 功能導向的項目,例如企業內部 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)


}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章