Kealdish's Studio.

CFNetwork学习笔记(四)

字数统计: 2.8k阅读时长: 11 min
2016/10/10 Share

前言

本篇文章主要讲述用CFHTTPAuthentication API如何与HTTP认证服务器交互,如何找到合适的认证对象和证书并填入到HTTP请求中。一般而言,如果HTTP服务器给你的HTTP请求返回401或407,就意味着服务器是认证服务器并且要求证书。在CFHTTPAuthentication API中,每个证书集合都被存储在CFHTTPAuthentication对象中。因此,每个不同的认证服务器和连接到服务器的不同用户都需要存储在独立的CFHTTPAuthentication对象中。为了能与服务器进行通信,你需要将CFHTTPAuthentication存储到HTTP请求当中。后面会对这些步骤进行详细解释。

认证

添加对认证的支持能让你的app与HTTP认证服务器进行会话,即使即使HTTP认证不是复杂的概念,但要去实现它却是比较复杂的过程。整个的过程如下:

1.客户端向服务器发送HTTP请求。
2.服务端要求客户端进行认证。
3.客户端将原始的请求和证书一起打包重新发给服务器。
4.客户端和服务器进行通信。
5.当服务器对客户端认证完成后,它会将请求的响应返回给客户端。

当HTTP请求返回401或407时,客户端第一步要做的是找到可靠的CFHTTPAuthentication对象。认证对象中包含证书和其他向服务器认证需要的信息。如果你已经跟服务器认证过了,你就会有有效的认证对象。然而,在大多数情况下,你需要调用CFHTTPAuthenticationCreateFromResponse方法创建认证对象。

1
2
3
4
5
6
7
8
9
10
11
if (!authentication) {
CFHTTPMessageRef responseHeader =
(CFHTTPMessageRef) CFReadStreamCopyProperty(
readStream,
kCFStreamPropertyHTTPResponseHeader
);
// Get the authentication information from the response.
authentication = CFHTTPAuthenticationCreateFromResponse(NULL, responseHeader);
CFRelease(responseHeader);
}

如果创建的认证对象是有效的,那就可以继续下一步了。如果认证对象是无效的,就检查看证书是否已经损坏。更多关于证书的信息,可以阅读Security Credentials中的内容。损坏的证书意味着服务器不接受登录信息并且继续监听新的证书。然而,如果证书是完好的而服务器拒绝了app发出的请求,那就是说服务器拒绝与app进行通信,这时候就必须放弃。在证书损坏的情况下,重新创建认证对象并重复认证的步骤直到得到有效的认证对象。以下为代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 查找有效的认证对象 Code 4-1
CFStreamError err;
if (!authentication) {
// the newly created authentication object is bad, must return
return;
} else if (!CFHTTPAuthenticationIsValid(authentication, &err)) {
// destroy authentication and credentials
if (credentials) {
CFRelease(credentials);
credentials = NULL;
}
CFRelease(authentication);
authentication = NULL;
// check for bad credentials (to be treated separately)
if (err.domain == kCFStreamErrorDomainHTTP &&
(err.error == kCFStreamErrorHTTPAuthenticationBadUserName
|| err.error == kCFStreamErrorHTTPAuthenticationBadPassword)){
retryAuthorizationFailure(&authentication);
return;
} else {
errorOccurredLoadingImage(err);
}
}

现在,你已经有了有效的认证对象,继续上面流程图下面的步骤。第一步,向服务器请求是否需要证书。如果不需要,直接将认证对象添加到HTTP请求当中。

1
2
3
4
5
6
7
8
9
10
void resumeWithCredentials() {
// Apply whatever credentials we've built up to the old request
if (!CFHTTPMessageApplyCredentialDictionary(request, authentication,
credentials, NULL)) {
errorOccurredLoadingImage();
} else {
// Now that we've updated our request, retry the load
loadRequest();
}
}

如果内存和磁盘中没有存储证书,获取有效证书的唯一方式是向用户请求。大部分情况下,证书都需要用户名和密码。将认证对象传入CFHTTPAuthenticationRequiresUserNameAndPassword方法中就可以知道用户名和密码是否是必须的。如果证书需要用户名和密码,想用户请求并将它们存储在证书的字典中。对于NTLM服务器而言,证书还需要提供域名。在你有了新的证书后,你可以调用resumeWithCredentials方法将认证对象添加到HTTP请求当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// ...continued from Code 4-1
else {
cancelLoad();
if (credentials) {
resumeWithCredentials();
}
// are a user name & password needed?
else if (CFHTTPAuthenticationRequiresUserNameAndPassword(authentication))
{
CFStringRef realm = NULL;
CFURLRef url = CFHTTPMessageCopyRequestURL(request);
// check if you need an account domain so you can display it if necessary
if (!CFHTTPAuthenticationRequiresAccountDomain(authentication)) {
realm = CFHTTPAuthenticationCopyRealm(authentication);
}
// ...prompt user for user name (user), password (pass)
// and if necessary domain (domain) to give to the server...
// Guarantee values
if (!user) user = CFSTR("");
if (!pass) pass = CFSTR("");
CFDictionarySetValue(credentials, kCFHTTPAuthenticationUsername, user);
CFDictionarySetValue(credentials, kCFHTTPAuthenticationPassword, pass);
// Is an account domain needed? (used currently for NTLM only)
if (CFHTTPAuthenticationRequiresAccountDomain(authentication)) {
if (!domain) domain = CFSTR("");
CFDictionarySetValue(credentials,
kCFHTTPAuthenticationAccountDomain, domain);
}
if (realm) CFRelease(realm);
CFRelease(url);
}
else {
resumeWithCredentials();
}
}

将证书保存在内存中

如果你需要频繁和认证服务器进行通信,那么重复利用证书可以避免多次向用户请求用户名和密码。为了重复利用证书,有三种数据结构需要在代码中做出修改。

1.创建可变数组CFMutableArrayRef取代CFHTTPAuthenticationRef对象来存储所有的认证对象
CFHTTPAuthenticationRef authentication; -> CFMutableArrayRef authArray;
2.使用字典创建认证对象到证书的映射
CFMutableDictionaryRef credentials; -> CFMutableDictionaryRef credentialsDict;
3.在所有更新当前认证对象和当前证书的地方保存这些结构
CFRelease(credentials); -> CFDictionaryRemoveValue(credentialsDict, authentication);

在创建HTTP请求后,需要查找匹配的认证对象。以下代码是很简易的且未优化的查找合适对象的方法:

1
2
3
4
5
6
7
8
9
10
11
CFHTTPAuthenticationRef findAuthenticationForRequest {
int i, c = CFArrayGetCount(authArray);
for (i = 0; i < c; i ++) {
CFHTTPAuthenticationRef auth = (CFHTTPAuthenticationRef)
CFArrayGetValueAtIndex(authArray, i);
if (CFHTTPAuthenticationAppliesToRequest(auth, request)) {
return auth;
}
}
return NULL;
}

如果认证数组中查找到匹配的认证对象,然后检查证书存储中是否可以获取。这样做防止你再次向用户请求用户名和密码。查找证书可以调用CFDictionaryGetValue方法。

1
credentials = CFDictionaryGetValue(credentialsDict, authentication);

然后将匹配的认证对象和证书添加到原始的HTTP请求中并重新发送。注意,在每一偶收到服务器认证消息之前不要将证书添加到HTTP请求中。服务器可能自从上一次认证过后改变了认证策略,你这样做可能会发生安全危险。

通过以上三种结构的改变,你的app能够将认证对象和证书存储在内存当中。

将证书保存在持久化仓库中

将证书存储在内存中可以防止在指定app启动时用户重复输入用户名和密码。然而,当app退出时,这些证书会被释放。为了避免丢失证书,我们将每个服务器的证书存储在持久化仓库中并且只需要生成一次。钥匙串是存储证书非常推荐的位置。即使你有多个钥匙串,默认会将用户的默认钥匙串作为存储证书的位置。使用钥匙串意味着你存储的认证信息同样可以在其他的app中使用。

在钥匙串中存储和获取证书依赖于两个方法:一个是用来查找认证的证书字典,另一个是用来存储请求中的证书。

1
2
3
4
CFMutableDictionaryRef findCredentialsForAuthentication(
CFHTTPAuthenticationRef auth);
void saveCredentialsForRequest(void);

findCredentialsForAuthentication方法首先会检查存储在内存中的证书字典是否在本地有缓存。如果证书在内存中没有缓存,之后就搜索钥匙串。调用SecKeychainFindInternetPassword方法搜索钥匙串。该方法要求很多参数:

keychainOrArray:NULL指定用户默认的钥匙串列表。
serverNameLength:服务器名字长度,通常是strlen(serverName)。
serverName:解析HTTP请求的服务器名字。
securityDomainLength:安全域名的长度,如果没有域名传入0。
securityDomain:认证对象的范围,可从CFHTTPAuthenticationCopyRealm方法中获取。
accountNameLength:用户名的长度。如果用户名为空,则传入0。
accountName:用户名。
pathLength:路径的长度,如果没有路径则传入0。
path:认证对象的路径,可从CFURLCopyPath方法中获取。
port:端口号,可从CFURLGetPortNumber方法中获取。
protocol:协议,可从CFURLCopyScheme方法中获取。
authenticationType:认证类型,可从CFHTTPAuthenticationCopyMethod方法中获取。
passwordLength:密码长度。0,因为在获取钥匙串许可时没有密码是必须的。
passwordData:密码数据。NULL,因为在获取钥匙串许可时没有密码是必须的。
itemRef:钥匙串条目。

1
2
3
4
5
6
7
8
9
10
11
didFind =
SecKeychainFindInternetPassword(NULL,
strlen(host), host,
realm ? strlen(realm) : 0, realm,
0, NULL,
path ? strlen(path) : 0, path,
port,
protocolType,
authenticationType,
0, NULL,
&itemRef);

SecKeychainFindInternetPassword方返回成功时,创建钥匙串属性列表(SecKeychainAttributeList)存储钥匙串属性(SecKeychainAttribute)。钥匙串属性列表会存储用户名和密码。要加载钥匙串属性列表,调用SecKeychainItemCopyContent方法并将SecKeychainFindInternetPassword方法返回的钥匙串条目对象传入。该方法会用用户的用户名和密码填充钥匙串属性。以下代码示例表示从要是传中加载服务器证书:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
if (didFind == noErr) {
SecKeychainAttribute attr;
SecKeychainAttributeList attrList;
UInt32 length;
void *outData;
// To set the account name attribute
attr.tag = kSecAccountItemAttr;
attr.length = 0;
attr.data = NULL;
attrList.count = 1;
attrList.attr = &attr;
if (SecKeychainItemCopyContent(itemRef, NULL, &attrList, &length, &outData)
== noErr) {
// attr.data is the account (username) and outdata is the password
CFStringRef username =
CFStringCreateWithBytes(kCFAllocatorDefault, attr.data,
attr.length, kCFStringEncodingUTF8, false);
CFStringRef password =
CFStringCreateWithBytes(kCFAllocatorDefault, outData, length,
kCFStringEncodingUTF8, false);
SecKeychainItemFreeContent(&attrList, outData);
// create credentials dictionary and fill it with the user name & password
credentials =
CFDictionaryCreateMutable(NULL, 0,
&kCFTypeDictionaryKeyCallBacks,
&kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(credentials, kCFHTTPAuthenticationUsername,
username);
CFDictionarySetValue(credentials, kCFHTTPAuthenticationPassword,
password);
CFRelease(username);
CFRelease(password);
}
CFRelease(itemRef);
}

如果你可以在钥匙串中存储证书,那么从钥匙串中获取证书会起作用。这些步骤与加载证书非常类似。首先,查看证书是否存储在要是传中。调用SecKeychainFindInternetPassword方法并传入用户名和用户名长度。如果证书存在,修改它的密码。设置data存入用户名,这样就可以修改正确的属性。然后调用SecKeychainItemModifyContent方法传入item、钥匙串属性列表和新密码。修改钥匙串许可而不是复写它,钥匙串许可会正确地更新并且任何与之关联的元数据都会得到保存。代码如下:

1
2
3
4
5
6
7
// Set the attribute to the account name
attr.tag = kSecAccountItemAttr;
attr.length = strlen(username);
attr.data = (void*)username;
// Modify the keychain entry
SecKeychainItemModifyContent(itemRef, &attrList, strlen(password), (void *)password);

如果许可不存在,你就需要创建它。SecKeychainAddInternetPassword方法可以完成这个工作,该方法的参数与SecKeychainFindInternetPassword方法一致,不一样的地方在于在SecKeychainAddInternetPassword方法中需要用户名和密码。成功调用后,若不需要再使用item则将其释放。

防火墙认证

防火墙认证与服务器认证很相似,但防火墙认证在HTTP请求失败后需要同时进行代理认证和服务器认证。这意味着你需要对代理服务器和原始服务器分开进行存储。因而,返回失败的响应后所做的操作应该是:

  • 检查响应码是不是407.如果是,在内存代理存储和磁盘代理存储中找到匹配的认证对象和证书。如果都没有合适的对象和证书,则向用户请求证书,然后将认证对象添加到HTTP请求中并重试。
  • 检查响应码是不是401。如果是,重复407下的做法,只是查找的是原始服务器存储。

除此之外,当使用代理服务器时还有些小差别。第一就是钥匙串调用方法的参数来自于代理主机和端口而不是原始服务器的URL。第二就是当向用户请求用户名和密码的时候,确保提示清楚地说明密码是什么。

只要遵循这些操作,app就能成功与认证防火墙工作。

CATALOG
  1. 1. 前言
  2. 2. 认证
  3. 3. 将证书保存在内存中
  4. 4. 将证书保存在持久化仓库中
  5. 5. 防火墙认证