登錄是一個現代App或者網站都必備的功能,對于開發者來說,這件事情的核心問題是
- 我到底需不需要登錄功能?
- 如果需要,那么在第三方登錄和第一方登錄之間如何做出選擇
- 如何設計一個第一方的登錄系統?
對于第一個問題,我覺得答案是肯定的,哪怕是一個單一功能的簡單App,那么登錄功能也是必須的。登錄可以給你帶來大量可分析的真實的用戶數據,這種基于真實用戶的數據要比從網上買來的dummy data更加適合一個企業對DT相關功能的探索和研發。同時登錄功能的存在可以更加好的提供客戶服務以及有利于數據傳輸的可靠性。在第三方登錄和第一方登錄這種問題上,第一方登錄能夠帶來更大的可控性,第三方登錄可以加快開發速度。第三方登錄系統基本都是基于OAuth系列的,相信大部分開發者都比較熟悉了,畢竟工作中,大量POC都是需要用到第三方登錄的。這篇文章將著重討論如何設計和實現一個登錄系統,同時,將涉及到一些諸如SSL加密通訊的相關話題。那么首先,對于前后端系統來說,到底什么叫做登錄?
首先,作為大環境的要求,單純的SSL證書加密和HTTPs是不足以應對今天更加復雜的網絡威脅的。一般基本的要求都是,對于server2server的API必須是2-way SSL,而mobile App因為性能上做不了2-way SSL,所以只能做cert pinning。作為最基本的前提,我們先來說說mobile app的cert pinning是什么。Cert pinning基本思想就是,通過預存在本地的footprint來對比server發過來的cert data。對于企業來說,一般都會有一個只存在于內網環境或者Dev環境的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的實現,然而這樣并不代表我們就安全了,就可以明碼傳輸數據了,數據還是進行加密處理的,比如信用卡卡號,用戶登錄的密碼等等。對于這些數據的加密,目前主流方案是使用sha256對數據進行加密處理,對于iOS平臺,我們可以對寫一個String的extension來實現數據加密:
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
}
}
到此為止,我們已經實現了最基本的登錄功能,但是這樣很明顯還是存在安全隱患:這個authentication是可以繞過去的。也就是說,我不登錄直接去使用其他API,那么我也是能夠獲得數據的。為了解決這個問題,authentication API在服務器端還需要生成一個one-time accessToken用作臨時密碼作為其他API驗證用戶的密碼。這個accessToken將會持續一段時間,銀行一般是15分鐘,如果服務器在這段時間內沒有收到新的request,這個token就會失效,用戶必須重新登錄來使用其他數據API。有的人會有疑問,你這不是重造輪子嗎?我們已經有一個存在了好多好多年的東西叫做cookie!事實上,在實際App中token-based authentication遠比cookie based流行,需要解釋為什么,我們先需要解釋另外一個概念叫做受信設備。
當App第一次被安裝到設備上時,在使用任何API之前,會先使用deviceToken API來生成一個device ID來用作該設備的device ID。以后在調用任何API的時候,這個id將被作為header的一部分傳遞到server。如果這個id不存在,server將自動觸發2-step authentication機制,比如向注冊手機號發送動態驗證碼之類的。而對于受信任的設備,這個時候,用戶可以選擇指紋登陸,然后API還是會返回一個token用于其他API驗證用戶。
所以token based到底比cookie based到底好在哪里?最重要的一點是token-based 是stateless,在restful的大環境下,無狀態依舊逐漸成為主流。因為這個token首先不需要儲存在數據庫當中因為是一次性的,其次和domain無關,最后在一些情況下將大幅減少服務器端所需要的操作。例如你的App是一個辦公App,經理,職員,ceo的權限是不一樣的,有了token,那么服務器端就不需要去驗證權限,對比權限而只需要驗證token本身是否有效。而cookie對移動端相對來說并不友好,一些老的API甚至壓根不支持移動端訪問,為了獲得cookie你甚至需要使用stealth webView來獲取cookie然后保存在本地供其他API使用。
總結一下,當點擊登陸按鈕的時候,到底發生了什么:
- 向服務器申請/調用本地device id
- 本地驗證用戶(指紋)/用戶名-密碼驗證用戶
- 得到服務器回傳的token并保存為一個全局變量或者class 變量。