版本记录
版本号 | 时间 |
---|---|
V1.0 | 2018.06.09 |
前言
CFNetwork框架访问网络服务并处理网络配置的变化。 建立在网络协议抽象的基础上,可以简化诸如使用BSD套接字,管理HTTP和FTP服务器以及管理Bonjour服务等任务。接下来几篇我们就一起看一下这个框架。感兴趣的可以看上面几篇文章。
1. CFNetwork框架详细解析(一) —— 基本概览
2. CFNetwork框架详细解析(二) —— CFNetwork编程指导之简介(一)
3. CFNetwork框架详细解析(三) —— CFNetwork编程指导之CFNetwork概念(二)
4. CFNetwork框架详细解析(四) —— CFNetwork编程指导之流的处理(三)
5. CFNetwork框架详细解析(五) —— CFNetwork编程指导之与HTTP服务器通信(四)
Communicating with Authenticating HTTP Servers - 与验证HTTP服务器通信
本章介绍如何利用CFHTTPAuthentication API
与验证HTTP服务器进行交互。 它解释了如何找到匹配的认证对象和证书,将它们应用于HTTP请求,并将其存储起来以备后用。
通常,如果HTTP服务器在HTTP请求之后返回401
或407
响应,则表示服务器正在进行身份验证并需要凭据。 在CFHTTPAuthentication API
中,每组证书都存储在一个CFHTTPAuthentication
对象中。 因此,每个不同的身份验证服务器和连接到该服务器的每个不同用户都需要一个单独的CFHTTPAuthentication
对象。 要与服务器通信,您需要将您的CFHTTPAuthentication
对象应用于HTTP请求。 接下来将更详细地解释这些步骤。
Handling Authentication - 处理验证
添加对身份验证的支持将允许您的应用程序与验证HTTP服务器进行通话(如果服务器返回401
或407
响应)。 尽管HTTP认证不是一个困难的概念,但它是一个复杂的过程。 程序如下:
- 客户端向服务器发送HTTP请求。
- 服务器向客户端返回质询。
- 客户端将原始请求与凭据捆绑在一起并将其发送回服务器。
- 客户端和服务器之间进行协商。
- 当服务器验证客户端时,它将回应请求。
执行此过程需要多个步骤。 整个过程的图表可以在图4-1
和图4-2
中看到。
当HTTP请求返回401或407响应时,第一步是让客户端找到一个有效的CFHTTPAuthentication
对象。 身份验证对象包含凭据和其他信息,这些信息在应用于HTTP消息请求时会验证您与服务器的身份。 如果您已经通过服务器验证过一次,您将拥有一个有效的验证对象。 但是,在大多数情况下,您需要使用CFHTTPAuthenticationCreateFromResponse
函数从响应中创建此对象。 参见Listing 4-1
。
注意:所有关于认证的示例代码都是从ImageClient应用程序改编的
Listing 4-1 Creating an authentication object
if (!authentication) {
CFHTTPMessageRef responseHeader =
(CFHTTPMessageRef) CFReadStreamCopyProperty(
readStream,
kCFStreamPropertyHTTPResponseHeader
);
// Get the authentication information from the response.
authentication = CFHTTPAuthenticationCreateFromResponse(NULL, responseHeader);
CFRelease(responseHeader);
}
如果新的认证对象是有效的,那么你就完成了,并且可以继续到图4-1
的第二步。 如果认证对象无效,则丢弃认证对象和凭证并检查凭证是否无效。 有关凭证的更多信息,请阅读Security Credentials。
错误的凭据意味着服务器不接受登录信息,它将继续侦听新的凭据。 但是,如果证书不错,但服务器仍然拒绝您的请求,那么服务器拒绝与您通话,所以您必须放弃。 假设证书不正确,请重新开始整个过程,直到获得工作证书和有效的验证对象为止。 在代码中,这个过程应该如Listing 4-2
所示。
Listing 4-2 Finding a valid authentication object
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);
}
}
现在您已拥有一个有效的认证对象,请继续遵循 Figure 4-1中的流程图。首先,确定你是否需要凭证。如果您不这样做,那么将认证对象应用于HTTP请求。认证对象应用于Listing 4-4中的HTTP请求(resumeWithCredentials)
。
如果不存储凭证(如在Keeping Credentials in Memory和Keeping Credentials in a Persistent Store中所述),获取有效凭证的唯一方法是提示用户。大多数情况下,凭证需要用户名和密码。通过将认证对象传递给CFHTTPAuthenticationRequiresUserNameAndPassword
函数,您可以查看是否需要用户名和密码。如果证书确实需要用户名和密码,请提示用户并将其存储在证书字典中。对于NTLM
服务器,凭据还需要域。获得新凭证后,可以使用Listing 4-4中的resumeWithCredentials
功能将认证对象应用于HTTP请求。整个过程如Listing 4-3
所示。
注意:在代码清单中,当注释以省略号开头和成功时,这意味着该操作不在本文档的范围内,但需要实施。这与描述正在发生的操作的一般注释不同。
Listing 4-3 Finding credentials (if necessary) and applying them
// ...continued from Listing 4-2
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();
}
}
Listing 4-4 Applying the authentication object to a request
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();
}
}
Keeping Credentials in Memory - 保存内存中的凭证
如果您打算经常与身份验证服务器进行通信,则可能需要重复使用凭据以避免多次提示用户输入服务器的用户名和密码。 本节介绍应对一次性使用身份验证代码(例如Handling Authentication中)进行的更改,以便将凭据存储在内存中供日后重用。
要重用凭证,需要对代码进行三项数据结构更改。
- 创建一个可变数组来容纳所有的认证对象。
CFMutableArrayRef authArray;
替换下面
CFHTTPAuthenticationRef authentication;
- 使用字典创建从认证对象到凭证的映射。
CFMutableDictionaryRef credentialsDict;
替换下面
CFMutableDictionaryRef credentials;
- 在您用来修改当前认证对象和当前凭证的任何位置维护这些结构。
CFDictionaryRemoveValue(credentialsDict, authentication);
替换下面
CFRelease(credentials);
现在,在创建HTTP请求之后,在每次加载之前查找匹配的认证对象。Listing 4-5
中可以看到一个简单的,未经优化的查找适当对象的方法
Listing 4-5 Looking for a matching authentication object
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
函数查找凭证,如Listing 4-6
所示。
Listing 4-6 Searching the credentials store
credentials = CFDictionaryGetValue(credentialsDict, authentication);
然后将您的匹配身份验证对象和凭据应用到您的原始HTTP请求并重新发送。
警告:在收到服务器质询之前,不要将凭据应用于HTTP请求。 自上次通过身份验证以来,服务器可能已发生更改,您可能会造成安全风险。
通过这些更改,您的应用程序将能够将认证对象和凭证存储在内存中供以后使用。
Keeping Credentials in a Persistent Store - 凭据的持久化存储
在内存中存储凭据可防止用户在特定应用程序启动期间不得不重新输入服务器的用户名和密码。 但是,当应用程序退出时,这些凭据将被释放。 为避免丢失凭据,请将其保存在持久性存储中,以便每个服务器的凭据只需生成一次。 钥匙串是存储凭证的推荐位置。 尽管您可以有多个钥匙串,但本文档将用户的默认钥匙串称为钥匙串。 使用钥匙串意味着您存储的身份验证信息也可以用于尝试访问同一服务器的其他应用程序,反之亦然。
存储和检索钥匙串中的凭证需要两个功能:一个用于查找用于验证的凭证字典,另一个用于保存最近请求的凭证。 这些函数将在本文档中声明为:
CFMutableDictionaryRef findCredentialsForAuthentication(
CFHTTPAuthenticationRef auth);
void saveCredentialsForRequest(void);
函数findCredentialsForAuthentication
首先检查存储在内存中的凭证字典,以查看凭证是否在本地缓存。有关如何实现这一点,请参见Listing 4-6。
如果凭证未缓存在内存中,则搜索钥匙串。要搜索钥匙串,请使用SecKeychainFindInternetPassword
函数。该函数需要大量的参数。这些参数以及它们如何与HTTP身份验证凭证一起使用的简短说明如下:
-
keychainOrArray
- NULL来指定用户的默认钥匙串列表。
-
serverNameLength
-
serverName
的长度,通常是strlen(serverName)
。
-
-
serverName
- 服务器名称从HTTP请求中解析。
-
securityDomainLength
- 安全域的长度,如果没有域,则为
0
。在示例代码中,realm ? strlen(realm) : 0
被传递给两种情况。
- 安全域的长度,如果没有域,则为
-
securityDomain
- 认证对象的领域,从
CFHTTPAuthenticationCopyRealm
函数获得。
- 认证对象的领域,从
-
accountNameLength
-
accountName
的长度。由于accountName
为NULL,因此该值为0。
-
-
accountName
- 获取钥匙串条目时没有帐户名称,所以这应该是
NULL
。
- 获取钥匙串条目时没有帐户名称,所以这应该是
-
pathLength
-
path
的长度,如果没有路径,则为0。在示例代码中,path ? strlen(path) : 0
传递给两种情况。
-
-
path
- 从
CFURLCopyPath
函数获取的认证对象的路径。
- 从
-
port
- 端口号,从函数 CFURLGetPortNumber 获取。
-
protocol
- 表示协议类型的字符串,例如 HTTP 或 HTTPS 。协议类型通过调用 CFURLCopyScheme 函数获得。
-
authenticationType
- 认证类型,从函数
CFHTTPAuthenticationCopyMethod
获得。
- 认证类型,从函数
-
passwordLength
- 0,因为在获取钥匙串条目时不需要密码。
-
passwordData
-
NULL
,因为在获取钥匙串条目时不需要密码。
-
-
itemRef
- 钥匙串项目引用对象,
SecKeychainItemRef
在找到正确的钥匙串条目后返回。
- 钥匙串项目引用对象,
正确调用时,代码应该如代码Listing 4-7
所示。
Listing 4-7 Searching the keychain
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
成功返回,请创建包含单个钥匙串属性(SecKeychainAttribute)
的钥匙串属性列表(SecKeychainAttributeList)
。 钥匙串属性列表将包含用户名和密码。 要加载Keychain
属性列表,请调用函数SecKeychainItemCopyContent
并将它传递给由SecKeychainFindInternetPassword
返回的Keychain项目引用对象(itemRef)
。 这个函数将用账号的用户名填充keychain
属性,并将一个void **
作为它的密码。
然后可以使用用户名和密码来创建一组新的凭证。 Listing 4-8
显示了这个过程。
Listing 4-8 Loading server credentials from the keychain
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
,但传递accountName
的用户名和accountNameLength
的accountName
的长度。
如果条目存在,请修改它以更改密码。 将Keychain属性的data
字段设置为包含用户名,以便修改正确的属性。 然后调用函数SecKeychainItemModifyContent
并传递钥匙串项目引用对象(itemRef)
,钥匙串属性列表和新密码。 通过修改钥匙串条目而不是覆盖它,钥匙链条目将被正确更新,并且任何关联的元数据仍将保留。 该条目应该与Listing 4-9
中的条目类似。
Listing 4-9 Modifying the keychain entry
// 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
相同,但与对SecKeychainFindInternetPassword
的调用相比,您提供了SecKeychainAddInternetPassword
用户名和密码。 在成功调用SecKeychainAddInternetPassword
后释放钥匙串项目引用对象,除非您需要将其用于其他内容。 请看Listing 4-10
中的函数调用。
Listing 4-10 Storing a new keychain entry
SecKeychainAddInternetPassword(NULL,
strlen(host), host,
realm ? strlen(realm) : 0, realm,
strlen(username), username,
path ? strlen(path) : 0, path,
port,
protocolType,
authenticationType,
strlen(password), password,
&itemRef);
Authenticating Firewalls - 认证防火墙
对防火墙进行身份验证与验证服务器非常相似,但必须检查每个失败的HTTP请求以进行代理身份验证和服务器身份验证。这意味着您需要为代理服务器和原始服务器分别存储(本地和永久)存储。因此,失败的HTTP响应的过程现在将是:
- 确定响应的状态代码是否是
407
(代理挑战)。如果是,则通过检查本地代理存储和持久代理存储找到匹配的认证对象和凭证。如果这两者都没有匹配的对象和凭证,则请求用户的凭证。将认证对象应用于HTTP请求并重试。 - 确定响应的状态代码是否是
401
(服务器质询)。如果是,请按照与407响应相同的步骤操作,但使用原始服务器存储。
使用代理服务器时还需要执行一些细微的差异。首先是钥匙串调用的参数来自代理主机和端口,而不是来自源服务器的URL。第二个是当询问用户的用户名和密码时,确保提示清楚地说明了密码的用途。
按照这些说明,您的应用程序应该能够使用认证防火墙。
后记
本篇主要讲述了与验证HTTP服务器通信,感兴趣的给个赞或者关注~~~