設計基於HTML5的APP登錄功能及安全調用接口的方式(原理篇)

設計基於HTML5的APP登錄功能及安全調用接口的方式(原理篇)

最近發現羣內大夥對用Hbuilder做的APP怎麼做登錄功能以及維護登錄狀態非常困惑,而我前一段時間正好稍微研究了一下,所以把我知道的告訴大家,節約大家查找資料的時間。

你是否真的需要登錄功能?

把這個問題放在最前面並不是灌水,而是真的見過很多並不需要登錄的APP去做了登錄功能,或者是並不需要強制登錄的APP把登錄作爲啓動頁。
用戶對你的APP一無所知,你就要求對方註冊並登錄,除非APP本身已經很有名氣或者是用戶有強需求,否則正常人應該會直接把它刪掉。
比較溫和的方式是將一些並不需要登錄,但可以給用戶帶來幫助的東西,第一時間展現給他們,讓他們產生興趣,再在合適的時機引導他們註冊(比如使用需要使用更高級的功能,或用戶需要收藏某個喜歡的信息時)。

登錄和註冊要足夠簡單

這是小小的手機端,用再好的輸入法,打字也是不方便的,所以別把登錄頁設計得需要填很多東西。如果有可能的話,只填手機號,讓用戶收到短信驗證碼就完成註冊是最好不過的了。想獲得更多信息?想想大公司的APP是怎麼做的,他們會告訴用戶,現在的個人資料完善程度是30%,如果想獲得更多積分,你需要填完。
tips:如果你想發佈在Appstore並且同時包含註冊功能,那麼註冊頁面必須做一個用戶許可協議的鏈接,否則有可能通不過審覈。

實現登錄後的session有幾種方式?

APP當瀏覽器用,直接載入遠程頁面

這種情況是很多偷懶的程序員或者傻X的老闆選擇的方式,因爲做起來實在太快。如果本身網站是響應式佈局,那麼很有可能不需要做什麼更改,就只要在開發時打開首頁就好了,這樣Hybird的APP外殼就純粹成爲了一個瀏覽器。
但比起這樣做帶來的無數缺點來,開發速度快的優點幾乎可以忽略不計。
首先,在網絡環境不佳時,純大白頁,用戶體驗0;
然後,CSS和JS等資源不在本地,需要遠程載入,如果使用了bootstrap之類的框架,那用戶爲了開一下APP而耗費的流量真是令人感動;
再然後,網頁裏常用的jquery,在手機的webview裏速度並不理想,而如果是非ajax的網頁那就更糟心了,每次操作都要跳轉和頁面渲染,要讓人把它當成APP那實在是笑話。
再再然後,這樣的所謂APP,要通過Appstore的審查,那是做夢的(除非審覈員當天鬧肚子嚴重,拿着紙巾奔向廁所前誤點了通過……),蘋果的要求是,這得是APP,而不能是某個網站做成APP的樣子,那樣的情況適合做Web APP。而據我所知,國內幾個較大的Android市場,這樣的APP也是無法通過審覈的。

調用後端接口

這是個很好的時代,因爲無論後端你是用Java、PHP,還是node.js,都可以通過xml、json來和APP通訊。遙想當年寫服務端要自己寫包結構,然後爲了解決併發問題還折騰了半年IOCP模型,真心覺得現在太幸福了。
把剛纔那個用APP當瀏覽器使的案例的所有缺點反過來看,就是這樣做的優點,在優化完善的情況下體驗接近原生,而且通訊流量極少,通過各種審覈也是妥妥的。
tips:通過plus對象中的XMLHttpRequest來Get、Post遠程的後端接口,或者使用Mui中封裝好的AJAX相關函數

插一段代碼,我把mui的ajax又做了進一步的封裝,對超時進行了自動重試,而對invalid_token等情況也做相應處理:

;mui.web_query = function(func_url, params, onSuccess, onError, retry){
    var onSuccess = arguments[2]?arguments[2]:function(){};
    var onError = arguments[3]?arguments[3]:function(){};
    var retry = arguments[4]?arguments[4]:3;
    func_url = 'http://www.xxxxxx.com/ajax/?fn=' + func_url;
    mui.ajax(func_url, {
        data:params,
        dataType:'json',
        type:'post',
        timeout:3000,
        success:function(data){
            if(data.err === 'ok'){
                onSuccess(data);
            }
            else{
                onError(data.code);
            }
        },
        error:function(xhr,type,errorThrown){
            retry--;
            if(retry > 0) return mui.web_query(func_url, params, onSuccess, onError, retry);
            onError('FAILED_NETWORK');
        }
    })};
var onError = function(errcode){
    switch(errcode){
    case 'FAILED_NETWORK':
        mui.toast('網絡不佳');
        break;
    case 'INVALID_TOKEN':
        wv_login.show();
        break;
    default:
        console.log(errcode);
    }};var params = {per:10, pageno:coms_current_pageno};
mui.web_query('get_com_list', params, onSuccess, onError, 3);

調用後端接口怎麼樣才安全?

在APP中保存登錄數據,每次調用接口時傳輸

程序員總能給自己找到偷懶的方法,有的程序爲了省事,會在用戶登錄後,直接把用戶名和密碼保存在本地,然後每次調用後端接口時作爲參數傳遞。真省事兒啊!可這種方法簡單就像拿着一袋子錢在路上邊走邊喊“快來搶我呀!快來搶我呀!”,一個小小的嗅探器就能把用戶的密碼拿到手,如果用戶習慣在所有地方用一個密碼,那麼你闖大禍了,黑客通過撞庫的方法能把用戶的所有信息一鍋端。

登錄時請求一次token,之後用token調用接口

這是比較安全的方式,用戶在登錄時,APP調用獲取token的接口(比如http://api.abc.com/get_token/),用post將用戶名和密碼的摘要傳遞給服務器,然後服務器比對數據庫中的用戶信息,匹配則返回綁定該用戶的token(這一般翻譯爲令牌,很直觀的名字,一看就知道是有了這玩意,就會對你放行),而數據庫中,在用戶的token表中也同時插入了這個token相關的數據:這個token屬於誰?這個token的有效期是多久?這個token當前登錄的ip地址是?這個token對應的deviceid是?……
這樣即便token被有心人截獲,也不會造成太大的安全風險。因爲沒有用戶名和密碼,然後如果黑客通過這個token僞造用戶請求,我們在服務器端接口被調用時就可以對發起請求的ip地址、user-agent之類的信息作比對,以防止僞造。再然後,如果token的有效期設得小,過一會兒它就過期了,除非黑客可以持續截獲你的token,否則他只能乾瞪眼。(插一句題外話:看到這裏,是不是明白爲什麼不推薦在外面隨便接入來歷不明的wifi熱點了?)
tips:token如何生成? 可以根據用戶的信息及一些隨機信息(比如時間戳)再通過hash編碼(比如md5、sha1等)生成唯一的編碼。
tips:token的安全級別,取決於你的實際需求,所以如果不是涉及財產安全的領域,並不建議太嚴格(比如用戶走着走着,3G換了個基站,閃斷了一下IP地址變了,尼瑪token過期了,這就屬於爲了不必要的安全丟了用戶體驗,當然如果變換的IP地址跨省的話還是應該驗證一下的,想想QQ有時候會讓填驗證碼就明白了)。
tips:接口在返回信息時,可以包含本次請求的狀態,比如成功調用,那麼result['status']可能就是'success',而反之則是'error',而如果是'error',則result['errcode']中就可以包含錯誤的原因,比如errcode中是'invalid_token'就可以告訴APP這個token過期或無效,這時APP應彈出登錄框或者用本地存儲的用戶名或密碼再次請求token(用戶選擇“記住密碼”,就應該在本地保存用戶名和密碼的摘要,方法見plus.storage的文檔)。

再插點代碼,基於plus.storage的用戶信息類,注意:需要在plusReady之後再使用。

;function UserInfo(){};//清除登錄信息UserInfo.clear = function(){
    plus.storage.removeItem('username');
    plus.storage.removeItem('password');
    plus.storage.removeItem('token');}//檢查是否包含自動登錄的信息UserInfo.auto_login = function(){
    var username = UserInfo.username();
    var pwd = UserInfo.password();
    if(!username || !pwd){
        return false;
    }
    return true;}//檢查是否已登錄UserInfo.has_login = function(){
    var username = UserInfo.username();
    var pwd = UserInfo.password();
    var token = UserInfo.token();
    if(!username || !pwd || !token){
        return false;
    }
    return true;};UserInfo.username = function(){
    if(arguments.length == 0){
        return plus.storage.getItem('username');        
    }
    if(arguments[0] === ''){
        plus.storage.removeItem('username');
        return;
    }
    plus.storage.setItem('username', arguments[0]);};UserInfo.password = function(){
    if(arguments.length == 0){
        return plus.storage.getItem('password');        
    }
    if(arguments[0] === ''){
        plus.storage.removeItem('password');
        return;
    }
    plus.storage.setItem('password', arguments[0]);};UserInfo.token = function(){
    if(arguments.length == 0){
        return plus.storage.getItem('token');       
    }
    if(arguments[0] === ''){
        plus.storage.removeItem('token');
        return;
    }
    plus.storage.setItem('token', arguments[0]);};

這樣當用戶啓動APP或使用了需要登錄才能使用的功能時,就可以使用UserInfo.has_login()來判斷是否已經登錄,如果已登錄,則使用UserInfo.token()來獲取到token數據,作爲參數調用遠程的後端接口。

if(UserInfo.has_login()){
    //打開需要展示給用戶的頁面,或者是調用遠端接口}else{
    wv_login.show('slide-in-up');   //從底部向上滑出登錄頁面}

在登錄頁面中,用戶輸入了用戶名和密碼後,並點擊了”登錄“按鈕,我們下一步做什麼?再插段代碼(注意:此處使用的是我剛纔代碼中擴展的web_query函數,你也可以直接使用mui的ajax):

function get_pwd_hash(pwd){
    var salt = 'hbuilder';  //此處的salt是爲了避免黑客撞庫,而在md5之前對原文做一定的變形,可以設爲自己喜歡的,只要和服務器驗證時的salt一致即可。
    return md5(salt + pwd); //此處假設你已經引用了md5相關的庫,比如github上的JavaScript-MD5}//這裏假設你已經通過DOM操作獲取到了用戶名和密碼,分別保存在username和password變量中。var username = xxx;var password = xxx;var pwd_hash = get_pwd_hash(password);var onSuccess = function(data){
    UserInfo.username(username);
    UserInfo.password(pwd_hash);
    UserInfo.token(data.token); //把獲取到的token保存到storage中
    var wc = plus.webview.currentWebview();
    wc.hide('slide-out-bottom');    //此處假設是隱藏登錄頁回到之前的頁面,實際你也可以乾點兒別的}var onError = function(errcode){
    switch(errcode){
    case 'INCORRECT_PASSWORD':
        mui.toast('密碼不正確');
        break;
    case 'USER_NOT_EXISTS':
        mui.toast('用戶尚未註冊');
        break;
    }}
mui.web_query('get_token', {username:username,password:pwd_hash}, onSuccess, onError, 3);

更安全一點,獲取token通過SSL

剛纔的方法,機智一點兒的讀者大概會心存疑慮:那獲取token時不還是得明文傳輸一次密碼嗎?
是的,你可以將這個獲取token的地址,用SSL來保護(比如https://api.abc.com/get_token/),這樣黑客即使截了包,一時半會兒也解不出什麼信息。
SSL證書的獲取渠道很多,我相信你總有辦法查到,所以不廢話了。不過話說namecheap上的SSL證書比godaddy的要便宜得多……(這是吐槽)
tips:前段時間OpenSSL漏洞讓很多服務器遭殃,所以如果自己搭服務器,一定記得裝補丁。
tips:可以把所有接口都弄成SSL的嗎?可以。但會拖慢服務器,如果是配置並不自信的VPS,建議不折騰。

還要更更安全(這標題真省事)

還記得剛纔APP向服務器請求token時,可以加入的用戶信息嗎?比如用戶的設備deviceid。
如果我們在調用接口時,還附帶一個當前時間戳參數timestamp,同時,用deviceid和這個時間戳再生成一個參數sign,比如 md5(deviceid timestamp token)這樣的形式。而服務端首先驗證一下參數中的時間戳與當前服務器時間是否一致(誤差保持在合理範圍內即可,比如5分鐘),然後根據用戶保存在服務器中的deviceid來對參數中的時間戳進行相同的變形,驗證是否匹配,那便自然“更更安全”了。
tips:如果對整個調用請求中的參數進行排序,再以deviceid和timestamp加上排序後的參數來對整個調用生成1個sign,黑客即使截獲sign,不同的時間點、參數請求所使用的sign也是不同的,難以僞造,自然會更安全。當然,寫起來也更費事。
tips:明白了原理,整個驗證過程是可以根據自己的需求改造的。


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