詳解AFNetworking的HTTPS模塊

0.0 簡述

文章內容包括:html

  • AFNetworking簡介
  • ATS和HTTPS介紹
  • AF中的證書驗證介紹
  • 如何建立服務端和客戶端自簽名證書
  • 如何建立簡單的https服務器
  • 對CA正式證書和自簽名證書的各類狀況進行代碼驗證

文中所涉及的文件和腳本代碼請看這裏node

1.0 AFNetworking簡介

AFNetworking(下面簡稱AF)是一個優秀的網絡框架,從事iOS開發工做的同窗幾乎都用過它。git

同時,AF也是一個簡單,高效的網絡框架。github

AF3.0版本(3.2.1)是對NSURLSession的封裝。NSURLSession是蘋果公司的HTTP協議實現,它儘量完整地實現了全部功能,可是同蘋果的Autolayout有相同的問題,就是API複雜難用。shell

所以在項目實踐中,即便咱們不使用AF,咱們也須要對NSURLSession進行適度封裝纔可以駕輕就熟。AF幫你作了這件事,並且可能作的更好。npm

AF將NSURLSession的複雜調用封裝到框架內部,並向外提供了更加簡單易懂的接口,它主要包含以下功能:json

  • 提供了AFHTTPSessionManager用於HTTP請求(GET,POST,...)
  • 提供AFURLRequestSerialization用於請求封裝,添加參數,設置header,傳遞數據
  • 提供AFURLResponseSerialization用於服務端返回數據的解析和過濾
  • 提供AFSecurityPolicy用於HTTPS協議證書驗證
  • 提供了AFNetworkReachabilityManager用於網絡狀態監聽
  • 提供了UIKit主要可用於圖片緩存,相似於SDWebImage

AF3.0的代碼足夠簡單,各個模塊也很容易理解,就不過多介紹了,咱們着重分析一下AFSecurityPolicy這個模塊。數組

2.0 ATS

iOS9.0版本中,包含了一個叫ATS的驗證機制,要求App網絡請求必須是安全的。主要包含2點:xcode

  1. 必須使用https
  2. https證書必須是公信機構頒發的證書

對於其中上面的第二點,在代碼層次沒有強制要求,使用自簽名證書也是能夠正常請求的,可能會在審覈階段有此要求。緩存

3.0 AF中的證書驗證

AF中實現了對服務端證書的驗證功能,驗證經過以後,便可正常進行網絡請求。

可是它沒有實現客戶端證書,因此若是服務器要求雙向驗證的時候,咱們就須要對AF進行一些擴展了。

關於https的介紹能夠參考這裏

服務端驗證證書的代碼在:AFURLSessionManager.m

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
        if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                if (credential) {
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        }
    }

    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

複製代碼

在NSURLSession中,當請求https的接口時,會觸發- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler回調,在這個回調中,你須要驗證服務端發送過來的證書,並返回一個NSURLCredential對象。

其中 disposition 這個變量用於表示你對證書的驗證結果,NSURLSessionAuthChallengeUseCredential表示驗證經過,其餘值都表示驗證失敗。

challenge.protectionSpace.authenticationMethod 這個枚舉字符串表示的是回調觸發的緣由,其中,NSURLAuthenticationMethodServerTrust表示服務端發來證書,NSURLAuthenticationMethodClientCertificate表示服務端請求驗證客戶端證書。

驗證證書的方法在AFSecurityPolicy.m中

- (BOOL)evaluateServerTrust:(SecTrustRef)serverTrust
                  forDomain:(NSString *)domain
{
    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."
        NSLog(@"In order to validate a domain name for self signed certificates, you MUST use pinning.");
        return NO;
    }

    NSMutableArray *policies = [NSMutableArray array];
    if (self.validatesDomainName) {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef)domain)];
    } else {
        [policies addObject:(__bridge_transfer id)SecPolicyCreateBasicX509()];
    }

    SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);

    if (self.SSLPinningMode == AFSSLPinningModeNone) {
        return self.allowInvalidCertificates || AFServerTrustIsValid(serverTrust);
    } else if (!AFServerTrustIsValid(serverTrust) && !self.allowInvalidCertificates) {
        return NO;
    }

    switch (self.SSLPinningMode) {
        case AFSSLPinningModeNone:
        default:
            return NO;
        case AFSSLPinningModeCertificate: {
            NSMutableArray *pinnedCertificates = [NSMutableArray array];
            for (NSData *certificateData in self.pinnedCertificates) {
                [pinnedCertificates addObject:(__bridge_transfer id)SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificateData)];
            }
            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);
            
            for (NSData *trustChainCertificate in [serverCertificates reverseObjectEnumerator]) {
                if ([self.pinnedCertificates containsObject:trustChainCertificate]) {
                    return YES;
                }
            }
            
            return NO;
        }
        case AFSSLPinningModePublicKey: {
            NSUInteger trustedPublicKeyCount = 0;
            NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

            for (id trustChainPublicKey in publicKeys) {
                for (id pinnedPublicKey in self.pinnedPublicKeys) {
                    if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                        trustedPublicKeyCount += 1;
                    }
                }
            }
            return trustedPublicKeyCount > 0;
        }
    }
    
    return NO;
}
複製代碼

代碼解析:

  • 函數第一行就是一長串的邏輯判斷,乍一看,這裏看的人很懵,它包含的信息不少。但實際上它的做用是用來處理服務端自簽名證書的。其餘狀況無需考慮此處邏輯。根據後面代碼來看,若是你服務端證書使用的是自簽名證書,AFSecurityPolicyallowInvalidCertificates屬性必須設爲YES,因此這裏判斷會帶上self.allowInvalidCertificates

  • 接下來就是驗證服務端證書的過程,SSLPinningMode 有3個值,AFSSLPinningModeNone表示服務端使用的是CA機構簽發的正式證書,另外2個值表示服務端使用的是自簽名證書。

  • AFServerTrustIsValid這個函數使用的是Security.framework中的方法,用於驗證服務端發送來的證書是不是可信任的,只要證書鏈中任何一個證書是已經信任的證書,那麼這個服務端證書就是合法的。詳細過程已經被Security.framework處理了,不須要咱們作額外工做。關於證書鏈能夠參考這裏

  • 第三部分代碼就是服務端自簽名證書的驗證了,這種狀況下,須要把服務端證書也放到客戶端中一份。根據SSLPinningMode,你能夠選擇使用服務端證書或者服務端證書內的公鑰

  • AFSSLPinningModeCertificate表示客戶端須要保存一個服務端根證書,用於驗證服務端證書是否合法。客戶端須要將服務端證書的證書鏈上的任意一個證書拖入xcode工程中。

  • 自簽名證書須要設置pinnedCertificates屬性,把拖入xcode的證書加載到內存中,保存在pinnedCertificates數組中。經過SecTrustSetAnchorCertificates方法把數組中的證書同服務端返回的證書作證書鏈綁定,而後就能夠用AFServerTrustIsValid方法驗證證書是否合法了,若是服務端證書和咱們客戶端保存的證書能夠正確匹配,這個函數就會返回YES。

  • AFSSLPinningModePublicKey表示客戶端須要保存一個服務端根證書公鑰,用於驗證服務端證書是否合法。客戶端須要將服務端證書鏈上的任意一證書的公鑰拖入xcode工程中。

  • 若使用公鑰驗證,則須要從服務端證書中取出公鑰,同時取出客戶端中保存的公鑰,逐一比較,若是有匹配的就認爲驗證成功。

根據上述分析,客戶端對於證書的使用,有下面的3種狀況:

  1. 服務端使用CA機構頒發的正式證書
  2. 服務端使用自簽名證書
  3. 服務端要驗證客戶端證書時,客戶端使用自簽名證書

4.0 證書驗證明踐

咱們對上面所述3種證書使用情形進行逐一驗證。

驗證以前,咱們須要作3個準備工做:

  • 第一是要把所需的證書建立出來
  • 第二是搭建簡單的服務器用於測試
  • 第三是建立客戶端工程引入AF3.0準備測試

4.1 建立證書

https使用的證書都是基於X.509格式的。

CA機構的正式證書通常是要花錢購買的,固然也有免費的,我以前在阿里雲買過免費的證書。通常申請經過後,你能夠把證書下載下來,其中主要包含私鑰和各類格式的證書。

自簽名的證書就比較容易了,在mac中可使用openssl命令來生成。

我寫了一個簡單的腳本,用於生成各類自簽名證書,你能夠把它保存到文件(文件名爲:create.sh)中,在終端裏執行。

腳本會生成3種證書:根證書,客戶端證書,服務端證書。

其中不一樣的證書沒有本質區別,只是用在不一樣的地方而已。

每種證書包含5個文件,分別是:

  • .der格式證書
  • .pem格式證書
  • .p12格式證書
  • .pem格式私鑰
  • .csr格式證書申請文件
#!/bin/sh

locale='CN' #地區
province='Beijing' #省份
city=$province #城市
company='xxx' #公司
unit='yyy' #部門
hostname='127.0.0.1' #域名
email='hr@suning.com' #郵箱
 #clean
function clean(){
	echo '清理文件...'
	ls | grep -v create.sh | xargs rm -rf
}
 #用法
function usage(){
	echo 'usage: ./create.sh 
		[-l [localevalue]]
		[-p [provincevalue]]
		[-c [cityvalue]]
		[-d [companyvalue]]
		[-u [unitvalue]]
		[-h [hostnamevalue]]
		[-e [emailvalue]]
	'
	exit
}
 #參數
if [ $# -gt 0 ]; then
	while getopts "cl:p:c:d:u:h:e" arg;
	do
		case $arg in
			c)
				clean && exit
				;;
			l)
				locale=$OPTARG
				;;
			p)
				province=$OPTARG
				;;
			c)
				city=$OPTARG
				;;
			d)
				company=$OPTARG
				;;
			u)
				unit=$OPTARG
				;;
			h)
				hostname=$OPTARG
				;;
			e)
				email=$OPTARG
				;;
			?)
				usage
				;;
		esac
	done
fi

clean

echo '開始建立根證書...'

openssl genrsa -out ca-private-key.pem 1024
openssl req -new -out ca-req.csr -key ca-private-key.pem <<EOF
${locale}
${province}
${city}
${company}
${unit}
${hostname}
${email}

EOF
openssl x509 -req -in ca-req.csr -out ca-cert.pem -outform PEM -signkey ca-private-key.pem -days 3650
openssl x509 -req -in ca-req.csr -out ca-cert.der -outform DER -signkey ca-private-key.pem -days 3650
echo '請輸入根證書p12文件密碼,直接回車表示密碼爲空字符串...'
openssl pkcs12 -export -clcerts -in ca-cert.pem -inkey ca-private-key.pem -out ca-cert.p12

echo '開始建立服務端證書...'

openssl genrsa -out server-private-key.pem 1024
openssl req -new -out server-req.csr -key server-private-key.pem << EOF
${locale}
${province}
${city}
${company}
${unit}
${hostname}
${email}

EOF
openssl x509 -req -in server-req.csr -out server-cert.pem -outform PEM -signkey server-private-key.pem -CA ca-cert.pem -CAkey ca-private-key.pem -CAcreateserial -days 3650
openssl x509 -req -in server-req.csr -out server-cert.der -outform DER -signkey server-private-key.pem -CA ca-cert.pem -CAkey ca-private-key.pem -CAcreateserial -days 3650
echo '請輸入服務端證書p12文件密碼,直接回車表示密碼爲空字符串...'
openssl pkcs12 -export -clcerts -in server-cert.pem -inkey server-private-key.pem -out server-cert.p12

echo '開始建立客戶端證書...'

openssl genrsa -out client-private-key.pem 1024
openssl req -new -out client-req.csr -key client-private-key.pem << EOF
${locale}
${province}
${city}
${company}
${unit}
${hostname}
${email}

EOF
openssl x509 -req -in client-req.csr -out client-cert.pem -outform PEM -signkey client-private-key.pem -CA ca-cert.pem -CAkey ca-private-key.pem -CAcreateserial -days 3650
openssl x509 -req -in client-req.csr -out client-cert.der -outform DER -signkey client-private-key.pem -CA ca-cert.pem -CAkey ca-private-key.pem -CAcreateserial -days 3650
echo '請輸入客戶端證書p12文件密碼,直接回車表示密碼爲空字符串...'
openssl pkcs12 -export -clcerts -in client-cert.pem -inkey client-private-key.pem -out client-cert.p12

echo 'finishied'

複製代碼

你能夠按照步驟操做:

  1. 複製腳本內容,保存到文件中,文件名爲create.sh

  2. 打開終端,經過cd命令進入create.sh所在的文件夾

  3. 在終端內輸入:chmod +x create.sh 點擊回車

  4. 在終端輸入:./create.sh -h,此時會打印用法

    usage: ./create.sh 
    	[-l [localevalue]]
    	[-p [provincevalue]]
    	[-c [cityvalue]]
    	[-d [companyvalue]]
    	[-u [unitvalue]]
    	[-h [hostnamevalue]]
    	[-e [emailvalue]]
    複製代碼

    腳本有下面幾種用法:

    • ./create.sh -h 打印用法
    • ./create.sh -c 會清空生成的全部文件
    • ./create.sh 直接回車,會使用默認參數生成證書
    • ./create.sh + 用法中所述選項 會使用自定義的參數生成證書

腳本執行成功後,應該會生成下面的文件:

4.2 搭建簡單的HTTPS服務器

咱們使用nodejs來搭建https服務器,請按照以下步驟操做:

  • 首先下載nodejs並安裝
  • 創建一個文件夾,文件夾內建立一個文件,名字爲package.json,內容以下:
{
    "name": "test-https",
    "version": "1.0.0",
    "main": "app.js",
    "scripts": {
        "start": "node app.js"
    },
    "debug": true,
    "dependencies": {
        "koa": "2.5.2",
        "koa-router": "7.4.0"
    }
}
複製代碼
  • 創建另外一個文件,名字爲app.js,內容以下:
const Koa = require('koa');
const https = require('https');
const fs = require('fs');
const router = require('koa-router')();

const app = new Koa();

//路由
router.get('/', (ctx, next) => {
    ctx.response.body = 'this is a simple node js https server response';
})
app.use(router.routes());

//https
https.createServer({
    key: fs.readFileSync('./yourServerCertPrivatekey.key'),
    cert: fs.readFileSync('./yourServerCert.pem'),
    requestCert: true,
    ca:[fs.readFileSync('./yourClientCert.pem')]
}, app.callback()).listen(3000);

console.log(`https app started at port 3000`)
複製代碼
  • 打開終端,使用cd命令進入咱們建立的服務器文件夾,而後執行命令:npm install,等待命令完成(可能會比較慢,根據網絡狀況而定)。如出現下列字樣表示安裝成功(不必定徹底相同):
added 40 packages from 21 contributors and audited 53 packages in 8.446s
found 0 vulnerabilities
複製代碼
  • 至此咱們的簡易https服務器就搭建完成了。咱們可使用命令:node app.js 來啓動服務器。可是你會發現會報錯,這是由於fs.readFileSync(filename)這句代碼表示要讀取一個證書文件,要確保文件存在才能夠。咱們後續根據需求來修改此處文件路徑便可。
  • 服務器啓動成功後,你能夠在終端看到下面的文字:
https app started at port 3000
複製代碼

4.3 創建客戶端工程

這個比較簡單,就很少說了。咱們使用下列基本代碼來作證書測試。

-(void) test{
	AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] init];
    
    //HTTPS驗證代碼,咱們主要修改這裏
    AFSecurityPolicy *policy = [AFSecurityPolicy defaultPolicy];
    policy.validatesDomainName = NO;//不驗證域名,是爲了測試方便,不然你須要修改host文件了
    manager.securityPolicy = policy;
    
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    //請求地址就寫這個
    [manager GET:@"https://127.0.0.1:3000/" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"succ and response = [%@]", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"fail");
    }];
}
複製代碼

4.4 服務端使用CA機構頒發的正式證書

這個是最簡單的狀況,AF已經支持,咱們不須要作任何額外工做就可以支持。

首先,咱們將服務端的代碼中的證書路徑指向咱們在CA機構申請好的服務端證書路徑,其中key表示證書私鑰,cert表示pem格式證書。另外將requestCertca這兩個字段先刪除,而後從新啓動服務器。像下面這樣:

... ...
//https
https.createServer({
    key: fs.readFileSync(這裏改爲你的私鑰路徑),
    cert: fs.readFileSync(這裏改爲你的pem格式證書路徑)
}, app.callback()).listen(3000);
... ...
複製代碼

而後,客戶端的代碼不須要修改。直接運行xcode,正常狀況下你能夠看到以下輸出:

succ and response = [this is a simple node js https server response]
複製代碼

4.5 服務端使用自簽名證書

服務端代碼不變,只是將證書和私鑰路徑修改成咱們自簽名的證書路徑。

上文中,咱們已經建立過自簽名的證書。

首先把證書文件夾的私鑰文件server-private-key.pem和證書文件server-cert.pem複製到服務器文件夾下。

而後服務器代碼修改以下:

... ...
//https
https.createServer({
    key: fs.readFileSync('./server-private-key.pem'),
    cert: fs.readFileSync('./server-cert.pem')
}, app.callback()).listen(3000);
... ...
複製代碼

重啓服務器。

客戶端須要把證書文件夾內的server-cert.der文件拖入xcode中,而後將xcode中的證書修更名字爲server-cert.cer

客戶端代碼作以下修改(請看註釋):

-(void) test{
	//使用服務器自簽名證書,須要指定baseUrl屬性。
    AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:@"https://127.0.0.1:3000"]];
    
    //AFSSLPinningModeCertificate表示使用自簽名證書
    AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    //爲了測試方便不驗證域名,若要驗證域名,則請求時的域名要和建立證書(建立證書的腳本執行時可指定域名)時的域名一致
    policy.validatesDomainName = NO;
    //自簽名服務器證書須要設置allowInvalidCertificates爲YES
    policy.allowInvalidCertificates = YES;
    //指定本地證書路徑
    policy.pinnedCertificates = [AFSecurityPolicy certificatesInBundle:[NSBundle mainBundle]];
    
    manager.responseSerializer = [AFHTTPResponseSerializer serializer];
    [manager GET:@"https://127.0.0.1:3000/" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"succ and response = [%@]", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"fail");
    }];
}
複製代碼

運行工程,正常狀況下,能夠看到正確輸出。

4.6 服務端驗證客戶端證書

這叫作雙向驗證,客戶端驗證服務端無誤以後,服務端也能夠驗證客戶端證書,這樣能夠保證數據傳輸雙方都是本身想要的目標。

首先,把證書文件夾內的client-cert.pem文件複製到服務器文件夾內。

而後修改服務端代碼:

... ...
//https
https.createServer({
    key: fs.readFileSync('./server-private-key.pem'),
    cert: fs.readFileSync('./server-cert.pem'),
    requestCert: true,//表示客戶端須要證書
    ca:[fs.readFileSync('./client-cert.pem')]//用於匹配客戶端證書
}, app.callback()).listen(3000);
... ...
複製代碼

重啓服務器。

客戶端須要把證書文件夾內的client-cert.p12文件拖到xcode中。

客戶端請求代碼不須要修改。

由於AF3.0並無提供對客戶端證書的支持,因此咱們須要修改AF的代碼。

找到AFURLSessionManager.m文件,在- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler方法。

- (void)URLSession:(NSURLSession *)session
didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge
 completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler
{
    NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling;
    __block NSURLCredential *credential = nil;

    if (self.sessionDidReceiveAuthenticationChallenge) {
        disposition = self.sessionDidReceiveAuthenticationChallenge(session, challenge, &credential);
    } else {
        NSString *authMethod = challenge.protectionSpace.authenticationMethod;
        if ([authMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
            if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
                credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
                if (credential) {
                    disposition = NSURLSessionAuthChallengeUseCredential;
                } else {
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            } else {
                disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge;
            }
        } else if([authMethod isEqualToString:NSURLAuthenticationMethodClientCertificate]){
            NSData *p12Data = [NSData dataWithContentsOfFile:[NSBundle pathForResource:@"client-cert" ofType:@"p12" inDirectory:[NSBundle mainBundle].bundlePath]];
            if([p12Data isKindOfClass:[NSData class]]){
                SecTrustRef trust = NULL;
                SecIdentityRef identity = NULL;
                [[self class] extractIdentity:&identity andTrust:&trust fromPKCS12Data:p12Data];
                if(identity){
                    SecCertificateRef certificate = NULL;
                    SecIdentityCopyCertificate(identity, &certificate);
                    const void *certs[] = {certificate};
                    CFArrayRef certArray =CFArrayCreate(kCFAllocatorDefault, certs, 1, NULL);
                    credential = [NSURLCredential credentialWithIdentity:identity certificates:(__bridge  NSArray*)certArray persistence:NSURLCredentialPersistencePermanent];
                    disposition = NSURLSessionAuthChallengeUseCredential;
                }else{
                    disposition = NSURLSessionAuthChallengePerformDefaultHandling;
                }
            }else{
                disposition = NSURLSessionAuthChallengePerformDefaultHandling;
            }
        } else {
            disposition = NSURLSessionAuthChallengePerformDefaultHandling;
        }
    }
    
    if (completionHandler) {
        completionHandler(disposition, credential);
    }
}

+ (BOOL)extractIdentity:(SecIdentityRef*)outIdentity andTrust:(SecTrustRef *)outTrust fromPKCS12Data:(NSData *)inPKCS12Data {
    OSStatus securityError = errSecSuccess;
    //客戶端證書密碼
    NSDictionary*optionsDictionary = [NSDictionary dictionaryWithObject: @""
                                                                 forKey: (__bridge id)kSecImportExportPassphrase];
    
    CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
    securityError = SecPKCS12Import((__bridge CFDataRef)inPKCS12Data,(__bridge CFDictionaryRef)optionsDictionary ,&items);
    
    if(securityError == 0) {
        CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex(items, 0);
        const void *tempIdentity = NULL;
        tempIdentity = CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemIdentity);
        *outIdentity = (SecIdentityRef)tempIdentity;
        const void *tempTrust = NULL;
        tempTrust = CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemTrust);
        *outTrust = (SecTrustRef)tempTrust;
        return YES;
    } else {
        NSLog(@"SecPKCS12Import is failed with error code %d", (int)securityError);
        return NO;
    }
}

複製代碼

上述代碼參考自這裏

值得注意的有2個地方:

  • 一個是p12文件的文件名,咱們這裏寫死了client-cert.p12,能夠根據具體狀況作修改。
  • 還有一個是p12文件的密碼,在extractIdentity:方法的第三行,能夠改爲你的p12文件密碼,密碼能夠爲空。

代碼修改好以後,運行工程,能夠獲得正確的服務端返回。

5.0 總結

文中內容均已通過測試,但仍然可能有錯誤之處,如發現請留言。

文中所涉及的腳本證書服務器代碼, 客戶端代碼已經上傳到github中,點這裏,都已經包含了安裝環境,下載後直接打開就能使用。

--完--

相關文章
相關標籤/搜索