天天看點

設計基于HTML5的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:明白了原理,整個驗證過程是可以根據自己的需求改造的。

-----------------------------------------------------華麗的分割線----------------------------------------------------

本文閱讀來源于http://ask.dcloud.net.cn/article/157

繼續閱讀