- 在APP中保存登錄數(shù)據(jù)问顷,每次調(diào)用接口時(shí)傳輸
程序員總能給自己找到偷懶的方法,有的程序?yàn)榱耸∈拢瑫?huì)在用戶登錄后杜窄,直接把用戶名和密碼保存在本地肠骆,然后每次調(diào)用后端接口時(shí)作為參數(shù)傳遞。真省事兒叭蚀腿!可這種方法簡(jiǎn)單就像拿著一袋子錢在路上邊走邊喊“快來(lái)?yè)屛已剑】靵?lái)?yè)屛已缴ㄍ猓 蔽ㄒВ粋€(gè)小小的嗅探器就能把用戶的密碼拿到手,如果用戶習(xí)慣在所有地方用一個(gè)密碼畏浆,那么你闖大禍了胆胰,黑客通過(guò)撞庫(kù)的方法能把用戶的所有信息一鍋端。
- 登錄時(shí)請(qǐng)求一次token刻获,之后用token調(diào)用接口
這是比較安全的方式蜀涨,用戶在登錄時(shí),APP調(diào)用獲取token的接口(比如http://api.abc.com/get_token/)蝎毡,用post將用戶名和密碼的摘要傳遞給服務(wù)器厚柳,然后服務(wù)器比對(duì)數(shù)據(jù)庫(kù)中的用戶信息,匹配則返回綁定該用戶的token(這一般翻譯為令牌沐兵,很直觀的名字别垮,一看就知道是有了這玩意,就會(huì)對(duì)你放行)扎谎,而數(shù)據(jù)庫(kù)中碳想,在用戶的token表中也同時(shí)插入了這個(gè)token相關(guān)的數(shù)據(jù):這個(gè)token屬于誰(shuí)?這個(gè)token的有效期是多久毁靶?這個(gè)token當(dāng)前登錄的ip地址是胧奔?這個(gè)token對(duì)應(yīng)的deviceid是?……
這樣即便token被有心人截獲预吆,也不會(huì)造成太大的安全風(fēng)險(xiǎn)龙填。因?yàn)闆](méi)有用戶名和密碼,然后如果黑客通過(guò)這個(gè)token偽造用戶請(qǐng)求拐叉,我們?cè)诜?wù)器端接口被調(diào)用時(shí)就可以對(duì)發(fā)起請(qǐng)求的ip地址岩遗、user-agent之類的信息作比對(duì),以防止偽造凤瘦。再然后宿礁,如果token的有效期設(shè)得小,過(guò)一會(huì)兒它就過(guò)期了廷粒,除非黑客可以持續(xù)截獲你的token窘拯,否則他只能干瞪眼。(插一句題外話:看到這里坝茎,是不是明白為什么不推薦在外面隨便接入來(lái)歷不明的wifi熱點(diǎn)了涤姊?)
tips:token如何生成? 可以根據(jù)用戶的信息及一些隨機(jī)信息(比如時(shí)間戳)再通過(guò)hash編碼(比如md5嗤放、sha1等)生成唯一的編碼思喊。
tips:token的安全級(jí)別,取決于你的實(shí)際需求次酌,所以如果不是涉及財(cái)產(chǎn)安全的領(lǐng)域恨课,并不建議太嚴(yán)格(比如用戶走著走著,3G換了個(gè)基站岳服,閃斷了一下IP地址變了剂公,尼瑪token過(guò)期了,這就屬于為了不必要的安全丟了用戶體驗(yàn)吊宋,當(dāng)然如果變換的IP地址跨省的話還是應(yīng)該驗(yàn)證一下的纲辽,想想QQ有時(shí)候會(huì)讓填驗(yàn)證碼就明白了)。
tips:接口在返回信息時(shí)璃搜,可以包含本次請(qǐng)求的狀態(tài)拖吼,比如成功調(diào)用,那么result['status']可能就是'success'这吻,而反之則是'error'吊档,而如果是'error',則result['errcode']中就可以包含錯(cuò)誤的原因唾糯,比如errcode中是'invalid_token'就可以告訴APP這個(gè)token過(guò)期或無(wú)效怠硼,這時(shí)APP應(yīng)彈出登錄框或者用本地存儲(chǔ)的用戶名或密碼再次請(qǐng)求token(用戶選擇“記住密碼”,就應(yīng)該在本地保存用戶名和密碼的摘要移怯,方法見(jiàn)plus.storage的文檔)拒名。
再插點(diǎn)代碼,基于plus.storage的用戶信息類芋酌,注意:需要在plusReady之后再使用增显。
function UserInfo(){
};
//清除登錄信息
UserInfo.clear = function(){
plus.storage.removeItem('username');
plus.storage.removeItem('password');
plus.storage.removeItem('token');
}
//檢查是否包含自動(dòng)登錄的信息
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]);
};
這樣當(dāng)用戶啟動(dòng)APP或使用了需要登錄才能使用的功能時(shí),就可以使用UserInfo.has_login()來(lái)判斷是否已經(jīng)登錄脐帝,如果已登錄同云,則使用UserInfo.token()來(lái)獲取到token數(shù)據(jù),作為參數(shù)調(diào)用遠(yuǎn)程的后端接口堵腹。
if(UserInfo.has_login()){
//打開(kāi)需要展示給用戶的頁(yè)面炸站,或者是調(diào)用遠(yuǎn)端接口
}
else{
wv_login.show('slide-in-up'); //從底部向上滑出登錄頁(yè)面
}
在登錄頁(yè)面中,用戶輸入了用戶名和密碼后疚顷,并點(diǎn)擊了”登錄“按鈕旱易,我們下一步做什么禁偎?再插段代碼(注意:此處使用的是我剛才代碼中擴(kuò)展的web_query函數(shù),你也可以直接使用mui的ajax):
function get_pwd_hash(pwd){
var salt = 'hbuilder'; //此處的salt是為了避免黑客撞庫(kù)阀坏,而在md5之前對(duì)原文做一定的變形如暖,可以設(shè)為自己喜歡的,只要和服務(wù)器驗(yàn)證時(shí)的salt一致即可忌堂。
return md5(salt + pwd); //此處假設(shè)你已經(jīng)引用了md5相關(guān)的庫(kù)盒至,比如github上的JavaScript-MD5
}
//這里假設(shè)你已經(jīng)通過(guò)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'); //此處假設(shè)是隱藏登錄頁(yè)回到之前的頁(yè)面枷遂,實(shí)際你也可以干點(diǎn)兒別的
}
var onError = function(errcode){
switch(errcode){
case 'INCORRECT_PASSWORD':
mui.toast('密碼不正確');
break;
case 'USER_NOT_EXISTS':
mui.toast('用戶尚未注冊(cè)');
break;
}
}
mui.web_query('get_token', {username:username,password:pwd_hash}, onSuccess, onError, 3);
- 更安全一點(diǎn),獲取token通過(guò)SSL
剛才的方法棋嘲,機(jī)智一點(diǎn)兒的讀者大概會(huì)心存疑慮:那獲取token時(shí)不還是得明文傳輸一次密碼嗎酒唉?
是的,你可以將這個(gè)獲取token的地址沸移,用SSL來(lái)保護(hù)(比如https://api.abc.com/get_token/)黔州,這樣黑客即使截了包,一時(shí)半會(huì)兒也解不出什么信息阔籽。
SSL證書的獲取渠道很多流妻,我相信你總有辦法查到,所以不廢話了笆制。不過(guò)話說(shuō)namecheap上的SSL證書比godaddy的要便宜得多……(這是吐槽)
tips:前段時(shí)間OpenSSL漏洞讓很多服務(wù)器遭殃绅这,所以如果自己搭服務(wù)器,一定記得裝補(bǔ)丁在辆。
tips:可以把所有接口都弄成SSL的嗎证薇?可以。但會(huì)拖慢服務(wù)器匆篓,如果是配置并不自信的VPS浑度,建議不折騰。
- 還要更更安全(這標(biāo)題真省事)
還記得剛才APP向服務(wù)器請(qǐng)求token時(shí)鸦概,可以加入的用戶信息嗎箩张?比如用戶的設(shè)備deviceid。
如果我們?cè)谡{(diào)用接口時(shí)窗市,還附帶一個(gè)當(dāng)前時(shí)間戳參數(shù)timestamp先慷,同時(shí),用deviceid和這個(gè)時(shí)間戳再生成一個(gè)參數(shù)sign咨察,比如 md5(deviceid timestamp token)這樣的形式论熙。而服務(wù)端首先驗(yàn)證一下參數(shù)中的時(shí)間戳與當(dāng)前服務(wù)器時(shí)間是否一致(誤差保持在合理范圍內(nèi)即可,比如5分鐘)摄狱,然后根據(jù)用戶保存在服務(wù)器中的deviceid來(lái)對(duì)參數(shù)中的時(shí)間戳進(jìn)行相同的變形脓诡,驗(yàn)證是否匹配无午,那便自然“更更安全”了。
tips:如果對(duì)整個(gè)調(diào)用請(qǐng)求中的參數(shù)進(jìn)行排序祝谚,再以deviceid和timestamp加上排序后的參數(shù)來(lái)對(duì)整個(gè)調(diào)用生成1個(gè)sign宪迟,黑客即使截獲sign,不同的時(shí)間點(diǎn)踊跟、參數(shù)請(qǐng)求所使用的sign也是不同的踩验,難以偽造鸥诽,自然會(huì)更安全商玫。當(dāng)然,寫起來(lái)也更費(fèi)事牡借。