登錄是一個現(xiàn)代App或者網(wǎng)站都必備的功能矿酵,對于開發(fā)者來說,這件事情的核心問題是
- 我到底需不需要登錄功能?
- 如果需要使鹅,那么在第三方登錄和第一方登錄之間如何做出選擇
- 如何設計一個第一方的登錄系統(tǒng)茎用?
對于第一個問題遣总,我覺得答案是肯定的,哪怕是一個單一功能的簡單App轨功,那么登錄功能也是必須的旭斥。登錄可以給你帶來大量可分析的真實的用戶數(shù)據(jù),這種基于真實用戶的數(shù)據(jù)要比從網(wǎng)上買來的dummy data更加適合一個企業(yè)對DT相關功能的探索和研發(fā)古涧。同時登錄功能的存在可以更加好的提供客戶服務以及有利于數(shù)據(jù)傳輸?shù)目煽啃粤鹪ぁT诘谌降卿浐偷谝环降卿涍@種問題上,第一方登錄能夠帶來更大的可控性蒿褂,第三方登錄可以加快開發(fā)速度圆米。第三方登錄系統(tǒng)基本都是基于OAuth系列的卒暂,相信大部分開發(fā)者都比較熟悉了,畢竟工作中娄帖,大量POC都是需要用到第三方登錄的也祠。這篇文章將著重討論如何設計和實現(xiàn)一個登錄系統(tǒng),同時近速,將涉及到一些諸如SSL加密通訊的相關話題诈嘿。那么首先,對于前后端系統(tǒng)來說削葱,到底什么叫做登錄奖亚?
首先,作為大環(huán)境的要求析砸,單純的SSL證書加密和HTTPs是不足以應對今天更加復雜的網(wǎng)絡威脅的昔字。一般基本的要求都是,對于server2server的API必須是2-way SSL首繁,而mobile App因為性能上做不了2-way SSL作郭,所以只能做cert pinning。作為最基本的前提弦疮,我們先來說說mobile app的cert pinning是什么夹攒。Cert pinning基本思想就是,通過預存在本地的footprint來對比server發(fā)過來的cert data胁塞。對于企業(yè)來說咏尝,一般都會有一個只存在于內(nèi)網(wǎng)環(huán)境或者Dev環(huán)境的cert server來提供cert給end dev:
%openssl s_client -showcerts -connect xxx.xxx.com:443 </dev/null 2>/dev/null|openssl x509 -outform DER > servercert.der
下載完了cert以后就可以單開一個project來將der文件轉化為footprint:
const unsigned char *dbytes = [data bytes];
NSMutableString *hexStr = [NSMutableString stringWithCapacity:[data length]*2];
int i;
for (i = 0; i < [data length]; i++) {
[hexStr appendFormat:@"0x%02x",dbytes[i]];
}
如果你將hexStr打印出來,將會在log里面看到類似“0x30,0x82.......”啸罢。復制粘貼這個string然后保存在一個[UInt8]里面状土,就可以直接使用了。
class WebServiceHandler:NSObject {
fileprivate let footPrint = [0x30,0x80,0x40.............]
func send<T:Request>(r:T,completion:DefaultCompletion) {
...
session = URLSession(conmfiguration:config,delegate:self,delegateQueue:PrivateQ)
...
}
}
extension WebServiceHandler:URLSessionDelegate {
func urlSession(_ session:URLSession, didReceive challenge:URLAuthenticationChallenge, comoletionHandler:@escaping (URLSession.AuthChallengeDisposition, URLCredential?)->Void) {
guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust else {
assert(false,"authentication not match")
return
}
guard let serverTrust = challenge.protectionSpace.serverTrust else {
assert(false,"cert not found")
return
}
guard SecTrustEvaluate(serverTrust, nil) == errSecSuccess else {
assert(false,"cert not match")
return
}
let count:CFIndex = SecTrustGetCertificateCount(serverTrust)
for i in 0..<count {
guard let certRef = SecTrustGetCertificateAtIndex(serverTrust, i) else {
assert(false,"invalid server cert")
continue
}
let certData = SecCertificateCopyData(certRef)
let remoteCert = certData as Data
let localCert = Data(bytes:footPrint)
if localCert == remoteCert {
completionHandler(.userCredential, URLCredential(trust:serverTrust))
break
}
}
...
}
}
這樣我們首先完成了對所有App-Server通訊的SSL Cert Pinning的實現(xiàn)伺糠,然而這樣并不代表我們就安全了蒙谓,就可以明碼傳輸數(shù)據(jù)了,數(shù)據(jù)還是進行加密處理的训桶,比如信用卡卡號累驮,用戶登錄的密碼等等。對于這些數(shù)據(jù)的加密舵揭,目前主流方案是使用sha256對數(shù)據(jù)進行加密處理谤专,對于iOS平臺,我們可以對寫一個String的extension來實現(xiàn)數(shù)據(jù)加密:
extension String {
func encriypt()->String {
if let stringData = self.data(using: .utf8) {
return hexStringFromData(stringData as NSData)
}
assert(false,"sha256 failure")
return self
}
private func digest(_ input:NSData) -> NSData {
let digestLength = Int(CC_SHA256_DIGEST_LENGTH)
var hash = [UInt8](repeating:0,count:digestLength)
CC_SHA256(input.bytes, UInt32(input.length),&hash)
return NSData(bytes:hash,length:digestLength)
}
private func hexStringFromData(_ input:NSData) -> String {
var bytes = [UInt8](repeating:0,count:input.length)
input.getBytes(&bytes, length:input.length)
var hexString = ""
for byte in bytes {
hexString += String(format:"%02x",UInt(byte))
}
return hexString
}
}
到此為止午绳,我們已經(jīng)實現(xiàn)了最基本的登錄功能置侍,但是這樣很明顯還是存在安全隱患:這個authentication是可以繞過去的。也就是說,我不登錄直接去使用其他API蜡坊,那么我也是能夠獲得數(shù)據(jù)的杠输。為了解決這個問題,authentication API在服務器端還需要生成一個one-time accessToken用作臨時密碼作為其他API驗證用戶的密碼秕衙。這個accessToken將會持續(xù)一段時間蠢甲,銀行一般是15分鐘,如果服務器在這段時間內(nèi)沒有收到新的request据忘,這個token就會失效鹦牛,用戶必須重新登錄來使用其他數(shù)據(jù)API。有的人會有疑問勇吊,你這不是重造輪子嗎曼追?我們已經(jīng)有一個存在了好多好多年的東西叫做cookie!事實上汉规,在實際App中token-based authentication遠比cookie based流行礼殊,需要解釋為什么,我們先需要解釋另外一個概念叫做受信設備鲫忍。
當App第一次被安裝到設備上時,在使用任何API之前钥屈,會先使用deviceToken API來生成一個device ID來用作該設備的device ID悟民。以后在調用任何API的時候,這個id將被作為header的一部分傳遞到server篷就。如果這個id不存在射亏,server將自動觸發(fā)2-step authentication機制,比如向注冊手機號發(fā)送動態(tài)驗證碼之類的竭业。而對于受信任的設備智润,這個時候,用戶可以選擇指紋登陸未辆,然后API還是會返回一個token用于其他API驗證用戶窟绷。
所以token based到底比cookie based到底好在哪里?最重要的一點是token-based 是stateless咐柜,在restful的大環(huán)境下兼蜈,無狀態(tài)依舊逐漸成為主流。因為這個token首先不需要儲存在數(shù)據(jù)庫當中因為是一次性的为狸,其次和domain無關,最后在一些情況下將大幅減少服務器端所需要的操作辐棒。例如你的App是一個辦公App,經(jīng)理漾根,職員泰涂,ceo的權限是不一樣的,有了token立叛,那么服務器端就不需要去驗證權限负敏,對比權限而只需要驗證token本身是否有效秘蛇。而cookie對移動端相對來說并不友好,一些老的API甚至壓根不支持移動端訪問赁还,為了獲得cookie你甚至需要使用stealth webView來獲取cookie然后保存在本地供其他API使用妖泄。
總結一下,當點擊登陸按鈕的時候艘策,到底發(fā)生了什么:
- 向服務器申請/調用本地device id
- 本地驗證用戶(指紋)/用戶名-密碼驗證用戶
- 得到服務器回傳的token并保存為一個全局變量或者class 變量蹈胡。