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

原文出處:http://ask.dcloud.net.cn/article/157


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com