文章内容包括:html
文中所涉及的文件和脚本代码请看这里。node
AFNetworking(下面简称AF)是一个优秀的网络框架,从事iOS开发工做的同窗几乎都用过它。git
同时,AF也是一个简单,高效的网络框架。github
AF3.0版本(3.2.1)是对NSURLSession的封装。NSURLSession是苹果公司的HTTP协议实现,它尽量完整地实现了全部功能,可是同苹果的Autolayout有相同的问题,就是API复杂难用。shell
所以在项目实践中,即便咱们不使用AF,咱们也须要对NSURLSession进行适度封装才可以驾轻就熟。AF帮你作了这件事,并且可能作的更好。npm
AF将NSURLSession的复杂调用封装到框架内部,并向外提供了更加简单易懂的接口,它主要包含以下功能:json
AF3.0的代码足够简单,各个模块也很容易理解,就不过多介绍了,咱们着重分析一下AFSecurityPolicy
这个模块。数组
iOS9.0版本中,包含了一个叫ATS的验证机制,要求App网络请求必须是安全的。主要包含2点:xcode
对于其中上面的第二点,在代码层次没有强制要求,使用自签名证书也是能够正常请求的,可能会在审核阶段有此要求。缓存
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;
}
复制代码
代码解析:
函数第一行就是一长串的逻辑判断,乍一看,这里看的人很懵,它包含的信息不少。但实际上它的做用是用来处理服务端自签名证书
的。其余状况无需考虑此处逻辑。根据后面代码来看,若是你服务端证书使用的是自签名证书,AFSecurityPolicy
的allowInvalidCertificates
属性必须设为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种状况:
咱们对上面所述3种证书使用情形进行逐一验证。
验证以前,咱们须要作3个准备工做:
https使用的证书都是基于X.509格式的。
CA机构的正式证书通常是要花钱购买的,固然也有免费的,我以前在阿里云买过免费的证书。通常申请经过后,你能够把证书下载下来,其中主要包含私钥和各类格式的证书。
自签名的证书就比较容易了,在mac中可使用openssl命令来生成。
我写了一个简单的脚本,用于生成各类自签名证书,你能够把它保存到文件(文件名为:create.sh)中,在终端里执行。
脚本会生成3种证书:根证书,客户端证书,服务端证书。
其中不一样的证书没有本质区别,只是用在不一样的地方而已。
每种证书包含5个文件,分别是:
#!/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'
复制代码
你能够按照步骤操做:
复制脚本内容,保存到文件中,文件名为create.sh
打开终端,经过cd
命令进入create.sh
所在的文件夹
在终端内输入:chmod +x create.sh
点击回车
在终端输入:./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 + 用法中所述选项
会使用自定义的参数生成证书脚本执行成功后,应该会生成下面的文件:
咱们使用nodejs来搭建https服务器,请按照以下步骤操做:
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
复制代码
node app.js
来启动服务器。可是你会发现会报错,这是由于fs.readFileSync(filename)
这句代码表示要读取一个证书文件,要确保文件存在才能够。咱们后续根据需求来修改此处文件路径便可。https app started at port 3000
复制代码
这个比较简单,就很少说了。咱们使用下列基本代码来作证书测试。
-(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");
}];
}
复制代码
这个是最简单的状况,AF已经支持,咱们不须要作任何额外工做就可以支持。
首先,咱们将服务端的代码中的证书路径指向咱们在CA机构申请好的服务端证书路径,其中key
表示证书私钥,cert
表示pem格式证书。另外将requestCert
和ca
这两个字段先删除,而后从新启动服务器。像下面这样:
... ...
//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]
复制代码
服务端代码不变,只是将证书和私钥路径修改成咱们自签名的证书路径。
上文中,咱们已经建立过自签名的证书。
首先把证书文件夹的私钥文件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");
}];
}
复制代码
运行工程,正常状况下,能够看到正确输出。
这叫作双向验证,客户端验证服务端无误以后,服务端也能够验证客户端证书,这样能够保证数据传输双方都是本身想要的目标。
首先,把证书文件夹内的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文件密码,密码能够为空。代码修改好以后,运行工程,能够获得正确的服务端返回。
文中内容均已通过测试,但仍然可能有错误之处,如发现请留言。
文中所涉及的脚本
,证书
,服务器代码
, 客户端代码
,已经上传到github中,点这里,都已经包含了安装环境,下载后直接打开就能使用。
--完--