CFNetwork(四)

Communicating with Authenticating HTTP Servers

@官方文檔翻譯-李冰

@譯文

本章介紹如何通過利用CFHTTPAuthentication API與驗證HTTP服務(wù)器進行交互片拍。它闡述了如何找到一個相匹配的認證對象和證書重抖,將它們應(yīng)用于一個HTTP請求,并保存它們供以后使用铜犬。

一般,如果一個HTTP服務(wù)器響應(yīng)返回401或407響應(yīng)你的請求后宴胧,這意味著服務(wù)器正在認證并需要證書。在CFHTTPAuthentication API中,任一證書集保存在CFHTTPAuthentication對象中肢藐。因此,每一個不同的認證服務(wù)器和每一個不同的用戶與服務(wù)器的連接都要求單獨的CFHTTPAuthentication對象吱韭。要與服務(wù)器的通信吆豹,你需要將你的CFHTTPAuthentication對象應(yīng)用HTTP請求。接下來更詳細的解釋這些步驟理盆。

Handling Authentication

添加對身份驗證的支持將允許您的應(yīng)用程序與身份驗證的HTTP服務(wù)器通信(如果服務(wù)器返回401或407響應(yīng))痘煤。盡管HTTP認證不是一個很難的概念,但是它是一個復(fù)雜的過程猿规。程序如下:

1.客戶端發(fā)送HTTP請求到服務(wù)器
2.服務(wù)器返回一個質(zhì)詢
3.客戶端使用證書捆綁原始請求衷快,并將其發(fā)送回給服務(wù)器
4.在客戶端和服務(wù)器之間進行協(xié)商。
5.當服務(wù)器驗證了客戶端姨俩,它將返回響應(yīng)給這一次請求蘸拔。

執(zhí)行這個過程需要多個步驟。整個過程的示意圖如圖4-1和圖4-2所示环葵。

圖 4-1 處理認證

apply_2x.png

Figure 4-2 查找認證對象

authentication_2x.png

當一個HTTP請求返回401或407響應(yīng)時调窍,首先在客戶端上查找一個有效的CFHTTPAuthentication對象。一個認證對象包含了證書和其他信息张遭,當應(yīng)用于一個HTTP消息請求時陨晶,將驗證你與服務(wù)器的身份。如果你已經(jīng)通過服務(wù)器認證一次帝璧,你將擁有一個有效的身份認證對象先誉。但是,在大多是情況下的烁,你需要使用CFHTTPAuthenticationCreateFromResponse函數(shù)從響應(yīng)中創(chuàng)建這個對象褐耳。見表4-1。

注意:有關(guān)身份驗證的所有示例代碼都從ImageClient應(yīng)用程序改寫的

表 4-1 創(chuàng)建一個認證對象

if (!authentication) {
    CFHTTPMessageRef responseHeader =
        (CFHTTPMessageRef) CFReadStreamCopyProperty(
            readStream,
            kCFStreamPropertyHTTPResponseHeader
        );
 
    // Get the authentication information from the response.
    authentication = CFHTTPAuthenticationCreateFromResponse(NULL, responseHeader);
    CFRelease(responseHeader);
}

如果新的認證對象有效渴庆,然后可以完成并繼續(xù)圖4-1的第二步铃芦。如果認證對象無效,則丟棄證書對象和證書并檢查證書是否損壞襟雷。
更多有關(guān)證書的詳細信息, 請閱讀 "Security Credentials".

錯誤證書意味著服務(wù)器不接受登錄信息并且將繼續(xù)監(jiān)聽新的證書刃滓。
但是,如果證書正確但是服務(wù)器仍拒絕你的請求耸弄,則服務(wù)器拒絕與你通信咧虎,所以你必須放棄分尸。

假定證書錯誤糯耍,請從開始創(chuàng)建證書對象重試整個過程直到獲得可工作的證書和有效的認證對象姚炕。在代碼中菩咨,這一過程看起來像表4-2。

表 4-2 查找到一個有效的認證對象

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);
    }
}

現(xiàn)在茁彭,你有一個有效的認證對象总寒,繼續(xù)按照圖4-1的流程圖。首先理肺,確定是否需要證書摄闸。如果不需要,則將認證對象應(yīng)用于HTTP請求妹萨。認證對象應(yīng)用于HTTP請求如表4-4(resumeWithCredentials)贪薪。

不用存儲證書 (如下文中Keeping Credentials in Memory 和 Keeping Credentials in a Persistent Store所訴), 獲取有效證書唯一方法是提示用戶。大多數(shù)時候眠副,證書需要用戶名和密碼画切。 通過將認證對象傳入CFHTTPAuthenticationRequiresUserNameAndPassword方法,你可以知道是否需要用戶名和密碼囱怕。如果證書確實需要用戶名和密碼霍弹,請?zhí)崾居脩籼峁┎⑵浯鎯υ谧C書字典中。對于NTLM服務(wù)器來說娃弓,證書還需要一個域典格。當你擁有一個新的證書,你可以使用resumeWithCredentials函數(shù)將認證對象應(yīng)用于HTTP請求如表4-4台丛。這所有的過程如表4-3所示耍缴。

提示: 在代碼列表中,當注釋以省略號開頭和結(jié)束時挽霉,意味著該操作不在本文檔的范圍內(nèi)防嗡,但需要實現(xiàn)。 這與描述正在發(fā)生什么操作的正常注釋不同侠坎。

表 4-3 查找證書(如果有必要)并應(yīng)用證書 Finding

// ...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();
    }
}

表 4-4 應(yīng)用認證對象去請求

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

如果你計劃經(jīng)常與一個認證服務(wù)器通信蚁趁,可能值得重復(fù)使用證書避免多次提示輸入服務(wù)器的賬號密碼。本節(jié)介紹了使用一次認證代碼(比如握手認證)存儲證書到內(nèi)存以便隨后使用的變化实胸。

要再次使用證書他嫡,你需要對你代碼進行三種數(shù)據(jù)結(jié)構(gòu)更改。

1.創(chuàng)建一個可變數(shù)組存儲所有的認證對象庐完。

CFMutableArrayRef authArray;

instead of:

CFHTTPAuthenticationRef authentication;

2.創(chuàng)建一個字典用于 認證對象到證書的映射钢属。

CFMutableDictionaryRef credentialsDict;

instead of:

CFMutableDictionaryRef credentials;

3.在您用于修改當前認證對象和當前憑據(jù)的任何位置維護這些結(jié)構(gòu)。

CFDictionaryRemoveValue(credentialsDict, authentication);

instead of:

CFRelease(credentials);

現(xiàn)在门躯,當創(chuàng)建HTTP請求后淆党,在每次加載之前查找一個匹配的認證對象。在表4-5中可以看到一個用于查找適當對象的簡單,未優(yōu)化的方法宁否。

表 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;
}

如果認證數(shù)組具有匹配的認證對象窒升,則檢查證書存儲缀遍,以查看正確的證書是否仍然有效慕匠。這樣做可以防止你再次提示用戶輸入用戶名和密碼。使用CFDictionaryGetValue函數(shù)查找證書域醇,如表4-6所示台谊。

表 4-6 Searching the credentials store

credentials = CFDictionaryGetValue(credentialsDict, authentication);

然后將匹配的認證對象和證書應(yīng)用到你的HTTP原始請求并重新發(fā)送。

警告: 在接收到服務(wù)器質(zhì)詢前不要講證書應(yīng)用到HTTP請求譬挚。服務(wù)器自上次驗證后可能已經(jīng)更改锅铅,你可能會創(chuàng)造安全風險。

通過這些更改减宣,你的應(yīng)用程序?qū)⒛軌蛉ピ趦?nèi)存中存儲認證對象和證書供稍后使用盐须。

Keeping Credentials in a Persistent Store

將證書存儲在內(nèi)存中可防止用戶在特定應(yīng)用程序啟動期間重新輸入服務(wù)器的用戶名和密碼。但是漆腌,當應(yīng)用程序退出后贼邓,這些證書將被釋放。為了防止證書丟失闷尿,保存證書到持久存儲中以便每一個服務(wù)器的證書只需要生成一次塑径。建議使用鑰匙串存儲證書。即時你有許多鑰匙串填具,本文檔將默認的鑰匙串稱為鑰匙串统舀。使用鑰匙串意味著你存儲的認證信息也可以在嘗試訪問同一應(yīng)用服務(wù)器的其他應(yīng)用程序中使用,反之亦然劳景。

在鑰匙串中存儲和檢索證書需要兩個函數(shù):一個是用于查找認證的證書字典誉简,另一個是保存最近請求的證書。這些函數(shù)將在本文檔中聲明為:

CFMutableDictionaryRef findCredentialsForAuthentication(
        CFHTTPAuthenticationRef auth);
 
void saveCredentialsForRequest(void);

findCredentialsForAuthentication函數(shù)首先檢查存儲在內(nèi)存中的證書字典盟广,以查看證書是否本地緩存描融。見表4-6如何實現(xiàn)這些。

如果證書未在內(nèi)存中緩存衡蚂,則搜索鑰匙串窿克。使用h SecKeychainFindInternetPassword函數(shù)搜索鑰匙串。這個函數(shù)需要大量的參數(shù)毛甲。參數(shù)以及如何用于HTTP認證證書的簡短說明如下:

keychainOrArray
NULL 指定用戶的默認鑰匙串列表年叮。
serverNameLength
serverName的長度, 通常是 strlen(serverName).
serverName
從HTTP請求解析的服務(wù)器名
securityDomainLength
安全域的長度, 如果沒有安全域則為0。 在示例代碼中, realm ? strlen(realm) : 0 被傳入用來考慮兩種情況玻募。
securityDomain
認證對象的范圍, 從 CFHTTPAuthenticationCopyRealm 函數(shù)中獲得只损。
accountNameLength
accountName的長度 。因為 accountNameNULL, 這個值是0.
accountName
獲取鑰匙串條目時沒有賬戶名, 因此這應(yīng)該是 NULL.
pathLength
path的長度, 如果沒有路徑則為0。 在示例代碼中, path ? strlen(path) : 0被傳入用來考慮兩種情況跃惫。
path
從認證對象獲取的路徑, 從CFURLCopyPath 函數(shù)中得到叮叹。
port
端口號, 從 CFURLGetPortNumber函數(shù)中得到。
protocol
代表協(xié)議類型的字符串, 比如 HTTP 或 HTTPS爆存。 調(diào)用 CFURLCopyScheme 函數(shù)獲得協(xié)議類型蛉顽。
authenticationType
認證類型, 從 CFHTTPAuthenticationCopyMethod函數(shù)獲得。
passwordLength
0, 因為獲取鑰匙串條目時不需要密碼先较。
passwordData
NULL,因為獲取鑰匙串條目時不需要密碼携冤。
itemRef,鑰匙串引用對象闲勺,SecKeychainItemRef曾棕,再找到正確的鑰匙串條目時返回。
當正確調(diào)用時菜循,代碼應(yīng)如表4-7所示翘地。

表 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);

假設(shè)SecKeychainFindInternetPassword 返回成功, 創(chuàng)建包含一個鑰匙串屬性(SecKeychainAttribute)的鑰匙串屬性列表 (SecKeychainAttributeList)。鑰匙串屬性列表將包含用戶名和密碼癌幕。調(diào)用函數(shù)SecKeychainItemCopyContent并且傳入SecKeychainFindInternetPassword返回的鑰匙串引用對象(itemRef)去加載鑰匙串屬性列表衙耕。這個函數(shù)將用用戶賬戶名和一個void **作為它的密碼填充鑰匙串屬性 。

用戶名和密碼可以被用于創(chuàng)建一組新的證書序芦。表4-8展示了這一過程臭杰。

表 4-8 從鑰匙串中加載服務(wù)器證書

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);
}

要從鑰匙串中檢索證書有用首先要求你可以將證書存儲在鑰匙串中。加載證書這一步非常簡單谚中。首先渴杆,查看證書是否已經(jīng)存儲在鑰匙串。調(diào)用SecKeychainFindInternetPassword宪塔,但需要傳入用戶名accountNameaccountName的長度accountNameLength磁奖。

如果條目存在,修改條目來更改密碼某筐。設(shè)置鑰匙串的data字段來包含用戶名比搭,以便你修改正確的屬性。隨后調(diào)用函數(shù)SecKeychainItemModifyContent并傳入鑰匙串引用對象(itemRef)南誊,鑰匙串屬性列表身诺,和新的密碼。
通過修改鑰匙串條目而不是覆蓋它抄囚,鑰匙串條目將被適當?shù)馗旅股模⑶胰魏蜗嚓P(guān)聯(lián)的元數(shù)據(jù)將仍然被保留。該條目應(yīng)類似于表4-9中的條目幔托。

表 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);

如果條目不存在穴亏,則你需要從頭開始創(chuàng)建蜂挪。函數(shù)SecKeychainAddInternetPassword完成這個任務(wù)。它的參數(shù)和SecKeychainFindInternetPassword一樣嗓化,但是與調(diào)用SecKeychainFindInternetPassword形成對比棠涮,你給SecKeychainAddInternetPassword提供用戶名和密碼。
在成功調(diào)用SecKeychainAddInternetPassword后刺覆,釋放鑰匙串項目引用對象严肪,除非你需要使用它做其他事情。 請參見代碼表4-10中的函數(shù)調(diào)用隅津。

表 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

驗證防火墻非常類似于驗證服務(wù)器诬垂,除了每個失敗的HTTP請求都必須檢查代理驗證和服務(wù)器驗證劲室。這意味著您需要為代理服務(wù)器和源服務(wù)器單獨存儲(本地和持久)伦仍。 因此,失敗的HTTP響應(yīng)的過程現(xiàn)在將是:

  • 確定響應(yīng)的狀態(tài)代碼是否為407(代理質(zhì)詢)很洋。 如果是充蓝,則通過檢查本地代理存儲和持久代理存儲來查找匹配的認證對象和憑據(jù)。 如果兩者都不具有匹配的對象和憑證喉磁,則請求用戶的憑證谓苟。 將身份驗證對象應(yīng)用于HTTP請求,然后重試协怒。
  • 確定響應(yīng)狀態(tài)代碼是否為401(服務(wù)器質(zhì)詢)涝焙。 如果是,請按照與407響應(yīng)相同的過程孕暇,但使用原始服務(wù)器存儲仑撞。

使用代理服務(wù)器時,還要強制執(zhí)行一些細微的差異妖滔。 第一個是鑰匙串調(diào)用的參數(shù)來自代理主機和端口隧哮,而不是來自源服務(wù)器的URL。 第二個是當向用戶請求用戶名和密碼時座舍,確保提示清楚地說明了密碼是什么沮翔。

按照這些說明,您的應(yīng)用程序應(yīng)該能夠使用驗證防火墻曲秉。


官方文檔

Communicating with Authenticating HTTP Servers

This chapter describes how to interact with authenticating HTTP servers by taking advantage of the CFHTTPAuthentication API. It explains how to find matching authentication objects and credentials, apply them to an HTTP request, and store them for later use.

In general, if an HTTP server returns a 401 or 407 response following your HTTP request, it means that the server is authenticating and requires credentials. In the CFHTTPAuthentication API, each set of credentials is stored in a CFHTTPAuthentication object. Therefore, every different authenticating server and every different user connecting to that server requires a separate CFHTTPAuthentication object. To communicate with the server, you need to apply your CFHTTPAuthentication object to the HTTP request. These steps are explained in more detail next.

Handling Authentication

Adding support for authentication will allow your application to talk with authenticating HTTP servers (if the server returns a 401 or 407 response). Even though HTTP authentication is not a difficult concept, it is a complicated process to execute. The procedure is as follows:

  1. The client sends an HTTP request to the server.
  2. The server returns a challenge to the client.
  3. The client bundles the original request with credentials and sends them back to the server.
  4. A negotiation takes place between the client and server.
  5. When the server has authenticated the client, it sends back the response to the request.

Performing this procedure requires a number of steps. A diagram of the entire procedure can be seen in Figure 4-1 and Figure 4-2.

Figure 4-1 Handling authentication

apply_2x.png

Figure 4-2 Finding an authentication object

authentication_2x.png

When an HTTP request returns a 401 or 407 response, the first step is for the client to find a valid CFHTTPAuthentication object. An authentication object contains credentials and other information that, when applied to an HTTP message request, verifies your identity with the server. If you've already authenticated once with the server, you will have a valid authentication object. However, in most cases, you will need to create this object from the response with the CFHTTPAuthenticationCreateFromResponse function. See Listing 4-1.

Note:All the sample code regarding authentication is adapted from the ImageClient application.

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);
}

If the new authentication object is valid, then you are done and can continue to the second step of Figure 4-1. If the authentication object is not valid, then throw away the authentication object and credentials and check to see if the credentials were bad. For more information about credentials, read "Security Credentials".

Bad credentials mean that the server did not accept the login information and it will continue to listen for new credentials. However, if the credentials were good but the server still rejected your request, then the server is refusing to speak with you, so you must give up. Assuming the credentials were bad, retry this entire process beginning with creating an authentication object until you get working credentials and a valid authentication object. In code, this procedure should look like the one in 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);
    }
}

Now that you have a valid authentication object, continue following the flowchart in Figure 4-1. First, determine whether you need credentials. If you don't, then apply the authentication object to the HTTP request. The authentication object is applied to the HTTP request in Listing 4-4 (resumeWithCredentials).

Without storing credentials (as explained in Keeping Credentials in Memory and Keeping Credentials in a Persistent Store), the only way to obtain valid credentials is by prompting the user. Most of the time, a user name and password are needed for the credentials. By passing the authentication object to the CFHTTPAuthenticationRequiresUserNameAndPassword function you can see if a user name and password are necessary. If the credentials do need a user name and password, prompt the user for them and store them in the credentials dictionary. For an NTLM server, the credentials also require a domain. After you have the new credentials, you can apply the authentication object to the HTTP request using the resumeWithCredentials function from Listing 4-4. This whole process is shown in Listing 4-3.

Note: In code listings, when comments are preceded and succeeded by ellipses, it means that that action is outside the scope of this document, but does need to be implemented. This is different from normal comments which describe what action is taking place.

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

If you plan on communicating with an authenticating server often, it may be worth reusing credentials to avoid prompting the user for the server's user name and password multiple times. This section explains the changes that should be made to one-time use authentication code (such as in Handling Authentication) to store credentials in memory for reuse later.

To reuse credentials, there are three data structure changes you need to make to your code.

  1. Create a mutable array to hold all the authentication objects.
CFMutableArrayRef authArray;

instead of:

CFHTTPAuthenticationRef authentication;
  1. Create a mapping from authentication objects to credentials using a dictionary.
CFMutableDictionaryRef credentialsDict;

instead of:

CFMutableDictionaryRef credentials;
  1. Maintain these structures everywhere you used to modify the current authentication object and the current credentials.
CFDictionaryRemoveValue(credentialsDict, authentication);

instead of:

CFRelease(credentials);

Now, after creating the HTTP request, look for a matching authentication object before each load. A simple, unoptimized method for finding the appropriate object can be seen in 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;
}

If the authentication array has a matching authentication object, then check the credentials store to see if the correct credentials are also available. Doing so prevents you from having to prompt the user for a user name and password again. Look for the credentials using the CFDictionaryGetValue function as shown in Listing 4-6.

Listing 4-6 Searching the credentials store

credentials = CFDictionaryGetValue(credentialsDict, authentication);

Then apply your matching authentication object and credentials to your original HTTP request and resend it.

Warning: Do not apply credentials to the HTTP request before receiving a server challenge. The server may have changed since the last time you authenticated and you could create a security risk.

With these changes, you application will be able to store authentication objects and credentials in memory for use later.

Keeping Credentials in a Persistent Store

Storing credentials in memory prevents a user from having to reenter a server's user name and password during that specific application launch. However, when the application quits, those credentials will be released. To avoid losing the credentials, save them in a persistent store so each server's credentials need to be generated only once. A keychain is the recommended place for storing credentials. Even though you can have multiple keychains, this document refers to the user's default keychain as the keychain. Using the keychain means that the authentication information that you store can also be used in other applications trying to access the same server, and vice versa.

Storing and retrieving credentials in the keychain requires two functions: one for finding the credentials dictionary for authentication and one for saving the credentials of the most recent request. These functions will be declared in this document as:

CFMutableDictionaryRef findCredentialsForAuthentication(
        CFHTTPAuthenticationRef auth);
 
void saveCredentialsForRequest(void);

The function findCredentialsForAuthentication first checks the credentials dictionary stored in memory to see whether the credentials are cached locally. See Listing 4-6 for how to implement this.

If the credentials are not cached in memory, then search the keychain. To search the keychain, use the function SecKeychainFindInternetPassword. This function requires a large number of parameters. The parameters, and a short description of how they are used with HTTP authentication credentials, are:

keychainOrArray
NULL to specify the user's default keychain list.
serverNameLength
The length of serverName, usually strlen(serverName).
serverName
The server name parsed from the HTTP request.
securityDomainLength
The length of security domain, or 0 if there is no domain. In the sample code, realm ? strlen(realm) : 0 is passed to account for both situations.
securityDomain
The realm of the authentication object, obtained from the CFHTTPAuthenticationCopyRealm function.
accountNameLength
The length of accountName. Since the accountName is NULL, this value is 0.
accountName
There is no account name when fetching the keychain entry, so this should be NULL.
pathLength
The length of path, or 0 if there is no path. In the sample code, path ? strlen(path) : 0 is passed to account for both situations.
path
The path from the authentication object, obtained from the CFURLCopyPath function.
port
The port number, obtained from the function CFURLGetPortNumber.
protocol
A string representing the protocol type, such as HTTP or HTTPS. The protocol type is obtained by calling the CFURLCopyScheme function.
authenticationType
The authentication type, obtained from the function CFHTTPAuthenticationCopyMethod.
passwordLength
0, because no password is necessary when fetching a keychain entry.
passwordData
NULL, because no password is necessary when fetching a keychain entry.
itemRef
The keychain item reference object, SecKeychainItemRef, returned upon finding the correct keychain entry
When called properly, the code should look like that in 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);

Assuming that SecKeychainFindInternetPassword returns successfully, create a keychain attribute list (SecKeychainAttributeList) containing a single keychain attribute (SecKeychainAttribute). The keychain attribute list will contain the user name and password. To load the keychain attribute list, call the function SecKeychainItemCopyContent and pass it the keychain item reference object (itemRef) that was returned by SecKeychainFindInternetPassword. This function will fill the keychain attribute with the account's user name, and a void ** as its password.

The user name and password can then be used to create a new set of credentials. Listing 4-8 shows this procedure.

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);
}

Retrieving credentials from the keychain is only useful if you can store credentials in the keychain first. The steps are very similar to loading credentials. First, see if the credentials are already stored in the keychain. Call SecKeychainFindInternetPassword, but pass the user name for accountName and the length of accountName for accountNameLength.

If the entry exists, modify it to change the password. Set the data field of the keychain attribute to contain the user name, so that you modify the correct attribute. Then call the function SecKeychainItemModifyContent and pass the keychain item reference object (itemRef), the keychain attribute list, and the new password. By modifying the keychain entry rather than overwriting it, the keychain entry will be properly updated and any associated metadata will still be preserved. The entry should look like the one in 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);

If the entry does not exist, then you will need to create it from scratch. The function SecKeychainAddInternetPassword accomplishes this task. Its parameters are the same as SecKeychainFindInternetPassword, but in contrast with the call to SecKeychainFindInternetPassword, you supply SecKeychainAddInternetPassword both a user name and a password. Release the keychain item reference object following a successful call to SecKeychainAddInternetPassword unless you need to use it for something else. See the function call in 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

Authenticating firewalls is very similar to authenticating servers except that every failed HTTP request must be checked for both proxy authentication and server authentication. This means that you need separate stores (both local and persistent) for proxy servers and origin servers. Thus, the procedure for a failed HTTP response will now be:

  • Determine whether the response's status code was 407 (a proxy challenge). If it is, find a matching authentication object and credentials by checking the local proxy store and the persistent proxy store. If neither of those has a matching object and credentials, then request the credentials from the user. Apply the authentication object to the HTTP request and try again.
  • Determine whether the response's status code was 401 (a server challenge). If it is, follow the same procedure as with a 407 response, but use the origin server stores.

There are also a few minor differences to enforce when using proxy servers. The first is that the arguments to the keychain calls come from the proxy host and port, rather than from the URL for an origin server. The second is that when asking the user for a user name and password, make sure the prompt clearly states what the password is for.

By following these instructions, your application should be able to work with authenticating firewalls.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末采蚀,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子承二,更是在濱河造成了極大的恐慌榆鼠,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件矢洲,死亡現(xiàn)場離奇詭異璧眠,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門责静,熙熙樓的掌柜王于貴愁眉苦臉地迎上來袁滥,“玉大人,你說我怎么就攤上這事灾螃√夥” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵腰鬼,是天一觀的道長嵌赠。 經(jīng)常有香客問我,道長熄赡,這世上最難降的妖魔是什么姜挺? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮彼硫,結(jié)果婚禮上炊豪,老公的妹妹穿的比我還像新娘。我一直安慰自己拧篮,他們只是感情好词渤,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著串绩,像睡著了一般缺虐。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上礁凡,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天高氮,我揣著相機與錄音,去河邊找鬼把篓。 笑死纫溃,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的韧掩。 我是一名探鬼主播紊浩,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼疗锐!你這毒婦竟也來了坊谁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤滑臊,失蹤者是張志新(化名)和其女友劉穎口芍,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體雇卷,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡鬓椭,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年颠猴,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片小染。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡翘瓮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出裤翩,到底是詐尸還是另有隱情资盅,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布踊赠,位于F島的核電站呵扛,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏筐带。R本人自食惡果不足惜今穿,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望烫堤。 院中可真熱鬧荣赶,春花似錦凤价、人聲如沸鸽斟。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽富蓄。三九已至,卻和暖如春慢逾,著一層夾襖步出監(jiān)牢的瞬間立倍,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工侣滩, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留口注,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓君珠,卻偏偏與公主長得像寝志,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子策添,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容