AFNetworking源码分析之安全策略

在网络请求中,我们经常使用http协议,http全称也就是超文本传输协议,通常使用版本是1.1版本,为了安全我们会使用https。这两个的区别就在于https是http经过加密的http,所以https更安全。正因为https有了加密这个过程,也使得https的响应速度没有http快。

http

http的历程
距今,http共有四个版本,分别是1991年的0.9版本的推出,这个版本仅支持GET命令;1996年推出了1.0版本,这个版本支持GET、POST、HEAD,增加了请求头,但是这个版本每次TCP连接只支持一次请求;半年后1997年就推出了我们现在使用最广泛的1.1版本,支持持久连接,还提出了管道机制;虽然1.1版本支持客户端同时发送多个请求,但是服务器的响应还是依次执行,所以后来推了2.0版本,这个版本就解决了服务器可以同时响应多个请求,还压缩了请求头,服务器还能自动推送静态资源

持久连接:就是指client和server之间保持持久连接,只有在双方长时间无响应的状态下才会断开连接。减少TCP连接的重复建立和断开所造成的额外开销
管道机制:同一个TCP连接可以发多个请求

http其实是无状态的,也就是说client和server之间不做状态保存,就减少了CPU和内存的消耗。但是这样就不能维持一个双向的会话,所以就用cookie来保存这个状态,比如我们开发中请求头中会有一个seeeionId什么的。

http的缺点:
1、明文传输,内容容易被窃听
2、没有验证通信方的身份,容易遭遇伪装
3、无法验证报文的完整性,可能遭遇篡改

因为如此,所以我们要对http进行加密。加密又分为通信加密和内容加密。内容加密容易被篡改,而通信加密通过ssl建立安全套接层,建立一个安全的套接线路,所以就有了https。

https

https都不能说是一种协议,他只是http之上做了一层ssl加密
ssl加密不仅提供了加密处理,还提供了证书,确保了通信线路的安全
https = http + 加密 + 认证 + 完整性保护

证书有CA证书和自签证书。CA证书是第三方机构给我们颁发的可信的证书

加密
1、共享密钥加密:加密和解密公用同一个密钥,又叫对称加密,比较快
2、公开私钥加密:有公开密钥和私有密钥,较慢

https上面两种加密都用了,在交换密钥使用公开密钥加密,在通信交换报文使用共享秘钥加密

https的缺点:通过ssl加密,处理速度会变慢,消耗CPU和内存资源,通信时间会延长。

AFSecurityPolicy

AFSecurityPolicy的作用就是客户端的证书验证,对于CA证书的验证我们不需要做什么,除非是自签证书,需要把证书放到本地。

如果是自签证书我们要做些什么呢?如下实例代码

- (void)test{
    NSString *urlstr = @"http://www.12306.cn/mormhweb/";
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    // 默认这个属性不用管,默认就是CA证书的认证,除非是银行等相关对安全要求比较高的应用,才会走自签证书的认证,才会去调用下面的方法
    manager.securityPolicy = [self securityPolicy];
    [manager GET:urlstr parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {
        
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        
    }];
}

- (AFSecurityPolicy *)securityPolicy{
    NSString *cerPath = [[NSBundle mainBundle] pathForResource:@"srca" ofType:@"cer"];
    NSData *cerData = [NSData dataWithContentsOfFile:cerPath];
    NSSet *set = [NSSet setWithObject:cerData];
    AFSecurityPolicy *securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate withPinnedCertificates:set];
    securityPolicy.allowInvalidCertificates = YES;
    securityPolicy.validatesDomainName = NO;
    return securityPolicy;
}

不管是CA证书还是自签证书的认证,AFN都已经为我们封装好,具体怎么实现,我们一步一步看源码

AFSSLPinningMode都有哪值呢?

typedef NS_ENUM(NSUInteger, AFSSLPinningMode) {
     //不使用固定证书(本地)验证服务器。直接从客户端系统中的受信任颁发机构 CA 列表中去验证,默认
    AFSSLPinningModeNone,
    // 代表会对服务器返回的证书中的PublicKey进行验证,通过则通过,否则不通过
    AFSSLPinningModePublicKey,
    // 代表会对服务器返回的证书同本地证书全部进行校验,通过则通过,否则不通过
    AFSSLPinningModeCertificate,
};

什么时候需要证书认证呢?就是在NSURLSessionDataDelegate的回调方法中

//收到服务端的challenge,例如https需要验证证书等 ats开启
- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{


    //挑战处理类型为 默认
    /*
     NSURLSessionAuthChallengeUseCredential:使用指定的证书
     NSURLSessionAuthChallengePerformDefaultHandling:默认方式处理
     NSURLSessionAuthChallengeCancelAuthenticationChallenge:取消挑战
     NSURLSessionAuthChallengeRejectProtectionSpace:拒绝此挑战,并尝试下一个验证保护空间;忽略证书参数
     */

    //挑战处理类型为默认
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;//证书

    // 自定义方法,用来如何应对服务器端的认证挑战
    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {

        // 1.判断接收服务器挑战的方法是否是信任证书
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            //只需要验证服务端证书是否安全(即https的单向认证,这是AF默认处理的认证方式,其他的认证方式,只能由我们自定义Block的实现
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                 // 2.信任评估通过,就从受保护空间里面拿出证书,回调给服务器,告诉服务,我信任你,你给我发送数据吧.
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
               // 确定挑战的方式
                if (credential) {
                    //证书挑战
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    //默认挑战
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                 //取消挑战
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else {
            //默认挑战方式
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }
 //完成挑战
    // 3.将信任凭证发送给服务端
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

其中校验最关键的方法就是AFSecurityPolicy提供的这个方法

-(BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(nullable NSString *)domain;

这个方法做了什么呢?我们看源码

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{

       //如果有服务器域名、设置了允许信任无效或者过期证书(自签名证书)、需要验证域名、没有提供证书或者不验证证书,返回no。后两者和allowInvalidCertificates为真的设置矛盾,说明这次验证是不安全的。
    if (domain && self.allowInvalidCertificates && self.validatesDomainName && (self.SSLPinningMode == AFSSLPinningModeNone || [self.pinnedCertificates count] == 0)) {
        // https://developer.apple.com/library/mac/documentation/NetworkingInternet/Conceptual/NetworkingTopics/Articles/OverridingSSLChainValidationCorrectly.html
        //  According to the docs, you should only trust your provided certs for evaluation.
        //  Pinned certificates are added to the trust. Without pinned certificates,
        //  there is nothing to evaluate against.
        //
        //  From Apple Docs:
        //          "Do not implicitly trust self-signed certificates as anchors (kSecTrustOptionImplicitAnchors).
        //           Instead, add your own (self-signed) CA certificate to the list of trusted anchors."
        //如果想要实现自签名的HTTPS访问成功,必须设置pinnedCertificates,且不能使用defaultPolicy
        NSLog(@"In order to val idate a domain name for self signed certificates, you MUST use pinning.");
        //不受信任,返回
        return NO;
    }
    //用来装验证策略
    NSMutableArray *policies = [NSMutableArray array];
    //生成验证策略。如果要验证域名,就以域名为参数创建一个策略,否则创建默认的basicX509策略
    if (self.validatesDomainName) {
         // 如果需要验证domain,那么就使用SecPolicyCreateSSL函数创建验证策略,其中第一个参数为true表示为服务器证书验证创建一个策略,第二个参数传入domain,匹配主机名和证书上的主机名
        //1.__bridge:CF和OC对象转化时只涉及对象类型不涉及对象所有权的转化
        //2.__bridge_transfer:常用在讲CF对象转换成OC对象时,将CF对象的所有权交给OC对象,此时ARC就能自动管理该内存
        //3.__bridge_retained:(与__bridge_transfer相反)常用在将OC对象转换成CF对象时,将OC对象的所有权交给CF对象来管理
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        // 如果不需要验证domain,就使用默认的BasicX509验证策略
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    // 为serverTrust设置验证策略,用策略对serverTrust进行评估
    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

//如果是AFSSLPinningModeNone(不做本地证书验证,从客户端系统中的受信任颁发机构 CA 列表中去验证服务端返回的证书)
    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        //不使用ssl pinning 但允许自建证书,直接返回YES;否则进行第二个条件判断,去客户端系统根证书里找是否有匹配的证书,验证serverTrust是否可信,直接返回YES
        
        //why需要allowInvalidCertificates?
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        //如果验证无效AFServerTrustIsValid,而且allowInvalidCertificates不允许自签,返回NO
        return NO;
    }

    switch (self.SSLPinningMode) {
//上一部分已经判断过了,如果执行到这里的话就返回NO
        case AFSSLPinningModeNone:
        default:
            return NO;
            //验证证书类型
            // 这个模式表示用证书绑定(SSL Pinning)方式验证证书,需要客户端保存有服务端的证书拷贝
            // 注意客户端保存的证书存放在self.pinnedCertificates中
        case AFSSLPinningModeCertificate: {
            // 全部校验(nsbundle .cer)
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            //把证书data,用系统api转成 SecCertificateRef 类型的数据,SecCertificateCreateWithData函数对原先的pinnedCertificates做一些处理,保证返回的证书都是DER编码的X.509证书
            for (NSData *certificateData in self.pinnedCertificates) {
                //cf arc brige:cf对象和oc对象转化 __bridge_transfer:把cf对象转化成oc对象
                //brige retain:oc转成cf对象
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            // 将pinnedCertificates设置成需要参与验证的Anchor Certificate(锚点证书,通过SecTrustSetAnchorCertificates设置了参与校验锚点证书之后,假如验证的数字证书是这个锚点证书的子节点,即验证的数字证书是由锚点证书对应CA或子CA签发的,或是该证书本身,则信任该证书),具体就是调用SecTrustEvaluate来验证
             //serverTrust是服务器来的验证,有需要被验证的证书
            // 把本地证书设置为根证书,
            SecTrustSetAnchorCertificates(serverTrust, (__bridge CFArrayRef)pinnedCertificates);
            
          //评估指定证书和策略的信任度(由系统默认可信或者由用户选择可信)
            if (!AFServerTrustIsValid(serverTrust)) {
                return NO;
            }

            // obtain the chain after being validated, which *should* contain the pinned certificate in the last position (if it's the Root CA)
            //注意,这个方法和我们之前的锚点证书没关系了,是去从我们需要被验证的服务端证书,去拿证书链。
            // 服务器端的证书链,注意此处返回的证书链顺序是从叶节点到根节点
            // 所有服务器返回的证书信息
            NSArray *serverCertificates = AFCertificateTrustChainForServerTrust(serverTrust);
            //reverseObjectEnumerator逆序
            // 倒序遍历
            //这里的证书链顺序是从叶节点到根节点
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                //如果我们的证书中,有一个和它证书链中的证书匹配的,就返回YES
                // 是否本地包含相同的data
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            //没有匹配的
            return NO;
        }
            //公钥验证 AFSSLPinningModePublicKey模式同样是用证书绑定(SSL Pinning)方式验证,客户端要有服务端的证书拷贝,只是验证时只验证证书里的公钥,不验证证书的有效期等信息。只要公钥是正确的,就能保证通信不会被窃听,因为中间人没有私钥,无法解开通过公钥加密的数据
 
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            // 从serverTrust中取出服务器端传过来的所有可用的证书,并依次得到相应的公钥
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);
            //遍历服务端公钥
            for (id trustChainPublicKey in publicKeys) {
                //遍历本地公钥
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    //判断如果相同 trustedPublicKeyCount+1
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容