登录这件小事

作者: ASAJ | 来源:发表于2017-05-23 04:21 被阅读0次

    登录是一个现代App或者网站都必备的功能,对于开发者来说,这件事情的核心问题是

    1. 我到底需不需要登录功能?
    2. 如果需要,那么在第三方登录和第一方登录之间如何做出选择
    3. 如何设计一个第一方的登录系统?

    对于第一个问题,我觉得答案是肯定的,哪怕是一个单一功能的简单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使用。

    总结一下,当点击登陆按钮的时候,到底发生了什么:

    1. 向服务器申请/调用本地device id
    2. 本地验证用户(指纹)/用户名-密码验证用户
    3. 得到服务器回传的token并保存为一个全局变量或者class 变量。

    相关文章

      网友评论

        本文标题:登录这件小事

        本文链接:https://www.haomeiwen.com/subject/szztxxtx.html