CFNetwork框架详细解析(六) —— CFNetwork编程指导之与验证HTTP服务器通信(五)

版本记录

版本号 时间
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请求之后返回401407响应,则表示服务器正在进行身份验证并需要凭据。 在CFHTTPAuthentication API中,每组证书都存储在一个CFHTTPAuthentication对象中。 因此,每个不同的身份验证服务器和连接到该服务器的每个不同用户都需要一个单独的CFHTTPAuthentication对象。 要与服务器通信,您需要将您的CFHTTPAuthentication对象应用于HTTP请求。 接下来将更详细地解释这些步骤。


Handling Authentication - 处理验证

添加对身份验证的支持将允许您的应用程序与验证HTTP服务器进行通话(如果服务器返回401407响应)。 尽管HTTP认证不是一个困难的概念,但它是一个复杂的过程。 程序如下:

  • 客户端向服务器发送HTTP请求。
  • 服务器向客户端返回质询。
  • 客户端将原始请求与凭据捆绑在一起并将其发送回服务器。
  • 客户端和服务器之间进行协商。
  • 当服务器验证客户端时,它将回应请求。

执行此过程需要多个步骤。 整个过程的图表可以在图4-1和图4-2中看到。

Figure 4-1 Handling authentication
Figure 4-2 Finding an authentication object

当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 MemoryKeeping 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的用户名和accountNameLengthaccountName的长度。

如果条目存在,请修改它以更改密码。 将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服务器通信,感兴趣的给个赞或者关注~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容