CFNetwork框架詳細(xì)解析(六) —— CFNetwork編程指導(dǎo)之與驗(yàn)證HTTP服務(wù)器通信(五)

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.06.09

前言

CFNetwork框架訪問網(wǎng)絡(luò)服務(wù)并處理網(wǎng)絡(luò)配置的變化。 建立在網(wǎng)絡(luò)協(xié)議抽象的基礎(chǔ)上,可以簡化諸如使用BSD套接字,管理HTTP和FTP服務(wù)器以及管理Bonjour服務(wù)等任務(wù)。接下來幾篇我們就一起看一下這個(gè)框架。感興趣的可以看上面幾篇文章。
1. CFNetwork框架詳細(xì)解析(一) —— 基本概覽
2. CFNetwork框架詳細(xì)解析(二) —— CFNetwork編程指導(dǎo)之簡介(一)
3. CFNetwork框架詳細(xì)解析(三) —— CFNetwork編程指導(dǎo)之CFNetwork概念(二)
4. CFNetwork框架詳細(xì)解析(四) —— CFNetwork編程指導(dǎo)之流的處理(三)
5. CFNetwork框架詳細(xì)解析(五) —— CFNetwork編程指導(dǎo)之與HTTP服務(wù)器通信(四)

Communicating with Authenticating HTTP Servers - 與驗(yàn)證HTTP服務(wù)器通信

本章介紹如何利用CFHTTPAuthentication API與驗(yàn)證HTTP服務(wù)器進(jìn)行交互。 它解釋了如何找到匹配的認(rèn)證對(duì)象和證書,將它們應(yīng)用于HTTP請(qǐng)求,并將其存儲(chǔ)起來以備后用。

通常,如果HTTP服務(wù)器在HTTP請(qǐng)求之后返回401407響應(yīng),則表示服務(wù)器正在進(jìn)行身份驗(yàn)證并需要憑據(jù)。 在CFHTTPAuthentication API中,每組證書都存儲(chǔ)在一個(gè)CFHTTPAuthentication對(duì)象中。 因此,每個(gè)不同的身份驗(yàn)證服務(wù)器和連接到該服務(wù)器的每個(gè)不同用戶都需要一個(gè)單獨(dú)的CFHTTPAuthentication對(duì)象。 要與服務(wù)器通信,您需要將您的CFHTTPAuthentication對(duì)象應(yīng)用于HTTP請(qǐng)求。 接下來將更詳細(xì)地解釋這些步驟。


Handling Authentication - 處理驗(yàn)證

添加對(duì)身份驗(yàn)證的支持將允許您的應(yīng)用程序與驗(yàn)證HTTP服務(wù)器進(jìn)行通話(如果服務(wù)器返回401407響應(yīng))。 盡管HTTP認(rèn)證不是一個(gè)困難的概念,但它是一個(gè)復(fù)雜的過程。 程序如下:

  • 客戶端向服務(wù)器發(fā)送HTTP請(qǐng)求。
  • 服務(wù)器向客戶端返回質(zhì)詢。
  • 客戶端將原始請(qǐng)求與憑據(jù)捆綁在一起并將其發(fā)送回服務(wù)器。
  • 客戶端和服務(wù)器之間進(jìn)行協(xié)商。
  • 當(dāng)服務(wù)器驗(yàn)證客戶端時(shí),它將回應(yīng)請(qǐng)求。

執(zhí)行此過程需要多個(gè)步驟。 整個(gè)過程的圖表可以在圖4-1和圖4-2中看到。

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

當(dāng)HTTP請(qǐng)求返回401或407響應(yīng)時(shí),第一步是讓客戶端找到一個(gè)有效的CFHTTPAuthentication對(duì)象。 身份驗(yàn)證對(duì)象包含憑據(jù)和其他信息,這些信息在應(yīng)用于HTTP消息請(qǐng)求時(shí)會(huì)驗(yàn)證您與服務(wù)器的身份。 如果您已經(jīng)通過服務(wù)器驗(yàn)證過一次,您將擁有一個(gè)有效的驗(yàn)證對(duì)象。 但是,在大多數(shù)情況下,您需要使用CFHTTPAuthenticationCreateFromResponse函數(shù)從響應(yīng)中創(chuàng)建此對(duì)象。 參見Listing 4-1

注意:所有關(guān)于認(rèn)證的示例代碼都是從ImageClient應(yīng)用程序改編的

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

如果新的認(rèn)證對(duì)象是有效的,那么你就完成了,并且可以繼續(xù)到圖4-1的第二步。 如果認(rèn)證對(duì)象無效,則丟棄認(rèn)證對(duì)象和憑證并檢查憑證是否無效。 有關(guān)憑證的更多信息,請(qǐng)閱讀Security Credentials。

錯(cuò)誤的憑據(jù)意味著服務(wù)器不接受登錄信息,它將繼續(xù)偵聽新的憑據(jù)。 但是,如果證書不錯(cuò),但服務(wù)器仍然拒絕您的請(qǐng)求,那么服務(wù)器拒絕與您通話,所以您必須放棄。 假設(shè)證書不正確,請(qǐng)重新開始整個(gè)過程,直到獲得工作證書和有效的驗(yàn)證對(duì)象為止。 在代碼中,這個(gè)過程應(yīng)該如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);
    }
}

現(xiàn)在您已擁有一個(gè)有效的認(rèn)證對(duì)象,請(qǐng)繼續(xù)遵循 Figure 4-1中的流程圖。首先,確定你是否需要憑證。如果您不這樣做,那么將認(rèn)證對(duì)象應(yīng)用于HTTP請(qǐng)求。認(rèn)證對(duì)象應(yīng)用于Listing 4-4中的HTTP請(qǐng)求(resumeWithCredentials)。

如果不存儲(chǔ)憑證(如在Keeping Credentials in MemoryKeeping Credentials in a Persistent Store中所述),獲取有效憑證的唯一方法是提示用戶。大多數(shù)情況下,憑證需要用戶名和密碼。通過將認(rèn)證對(duì)象傳遞給CFHTTPAuthenticationRequiresUserNameAndPassword函數(shù),您可以查看是否需要用戶名和密碼。如果證書確實(shí)需要用戶名和密碼,請(qǐng)?zhí)崾居脩舨⑵浯鎯?chǔ)在證書字典中。對(duì)于NTLM服務(wù)器,憑據(jù)還需要域。獲得新憑證后,可以使用Listing 4-4中的resumeWithCredentials功能將認(rèn)證對(duì)象應(yīng)用于HTTP請(qǐng)求。整個(gè)過程如Listing 4-3所示。

注意:在代碼清單中,當(dāng)注釋以省略號(hào)開頭和成功時(shí),這意味著該操作不在本文檔的范圍內(nèi),但需要實(shí)施。這與描述正在發(fā)生的操作的一般注釋不同。

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 - 保存內(nèi)存中的憑證

如果您打算經(jīng)常與身份驗(yàn)證服務(wù)器進(jìn)行通信,則可能需要重復(fù)使用憑據(jù)以避免多次提示用戶輸入服務(wù)器的用戶名和密碼。 本節(jié)介紹應(yīng)對(duì)一次性使用身份驗(yàn)證代碼(例如Handling Authentication中)進(jìn)行的更改,以便將憑據(jù)存儲(chǔ)在內(nèi)存中供日后重用。

要重用憑證,需要對(duì)代碼進(jìn)行三項(xiàng)數(shù)據(jù)結(jié)構(gòu)更改。

  • 創(chuàng)建一個(gè)可變數(shù)組來容納所有的認(rèn)證對(duì)象。
CFMutableArrayRef authArray;

替換下面

CFHTTPAuthenticationRef authentication;
  • 使用字典創(chuàng)建從認(rèn)證對(duì)象到憑證的映射。
CFMutableDictionaryRef credentialsDict;

替換下面

CFMutableDictionaryRef credentials;
  • 在您用來修改當(dāng)前認(rèn)證對(duì)象和當(dāng)前憑證的任何位置維護(hù)這些結(jié)構(gòu)。
CFDictionaryRemoveValue(credentialsDict, authentication);

替換下面

CFRelease(credentials);

現(xiàn)在,在創(chuàng)建HTTP請(qǐng)求之后,在每次加載之前查找匹配的認(rèn)證對(duì)象。Listing 4-5中可以看到一個(gè)簡單的,未經(jīng)優(yōu)化的查找適當(dāng)對(duì)象的方法

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

如果身份驗(yàn)證數(shù)組中具有匹配的身份驗(yàn)證對(duì)象,請(qǐng)檢查憑據(jù)存儲(chǔ)以查看是否也有可用的正確憑據(jù)。 這樣做可以防止您再次提示用戶輸入用戶名和密碼。 使用CFDictionaryGetValue函數(shù)查找憑證,如Listing 4-6所示。

Listing 4-6  Searching the credentials store

credentials = CFDictionaryGetValue(credentialsDict, authentication);

然后將您的匹配身份驗(yàn)證對(duì)象和憑據(jù)應(yīng)用到您的原始HTTP請(qǐng)求并重新發(fā)送。

警告:在收到服務(wù)器質(zhì)詢之前,不要將憑據(jù)應(yīng)用于HTTP請(qǐng)求。 自上次通過身份驗(yàn)證以來,服務(wù)器可能已發(fā)生更改,您可能會(huì)造成安全風(fēng)險(xiǎn)。

通過這些更改,您的應(yīng)用程序?qū)⒛軌驅(qū)⒄J(rèn)證對(duì)象和憑證存儲(chǔ)在內(nèi)存中供以后使用。


Keeping Credentials in a Persistent Store - 憑據(jù)的持久化存儲(chǔ)

在內(nèi)存中存儲(chǔ)憑據(jù)可防止用戶在特定應(yīng)用程序啟動(dòng)期間不得不重新輸入服務(wù)器的用戶名和密碼。 但是,當(dāng)應(yīng)用程序退出時(shí),這些憑據(jù)將被釋放。 為避免丟失憑據(jù),請(qǐng)將其保存在持久性存儲(chǔ)中,以便每個(gè)服務(wù)器的憑據(jù)只需生成一次。 鑰匙串是存儲(chǔ)憑證的推薦位置。 盡管您可以有多個(gè)鑰匙串,但本文檔將用戶的默認(rèn)鑰匙串稱為鑰匙串。 使用鑰匙串意味著您存儲(chǔ)的身份驗(yàn)證信息也可以用于嘗試訪問同一服務(wù)器的其他應(yīng)用程序,反之亦然。

存儲(chǔ)和檢索鑰匙串中的憑證需要兩個(gè)功能:一個(gè)用于查找用于驗(yàn)證的憑證字典,另一個(gè)用于保存最近請(qǐng)求的憑證。 這些函數(shù)將在本文檔中聲明為:

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

函數(shù)findCredentialsForAuthentication首先檢查存儲(chǔ)在內(nèi)存中的憑證字典,以查看憑證是否在本地緩存。有關(guān)如何實(shí)現(xiàn)這一點(diǎn),請(qǐng)參見Listing 4-6。

如果憑證未緩存在內(nèi)存中,則搜索鑰匙串。要搜索鑰匙串,請(qǐng)使用SecKeychainFindInternetPassword函數(shù)。該函數(shù)需要大量的參數(shù)。這些參數(shù)以及它們?nèi)绾闻cHTTP身份驗(yàn)證憑證一起使用的簡短說明如下:

  • keychainOrArray

    • NULL來指定用戶的默認(rèn)鑰匙串列表。
  • serverNameLength

    • serverName的長度,通常是strlen(serverName)。
  • serverName

    • 服務(wù)器名稱從HTTP請(qǐng)求中解析。
  • securityDomainLength

    • 安全域的長度,如果沒有域,則為0。在示例代碼中,realm ? strlen(realm) : 0被傳遞給兩種情況。
  • securityDomain

    • 認(rèn)證對(duì)象的領(lǐng)域,從CFHTTPAuthenticationCopyRealm函數(shù)獲得。
  • accountNameLength

    • accountName的長度。由于accountName為NULL,因此該值為0。
  • accountName

    • 獲取鑰匙串條目時(shí)沒有帳戶名稱,所以這應(yīng)該是NULL。
  • pathLength

    • path的長度,如果沒有路徑,則為0。在示例代碼中, path ? strlen(path) : 0傳遞給兩種情況。
  • path

    • CFURLCopyPath函數(shù)獲取的認(rèn)證對(duì)象的路徑。
  • port

    • 端口號(hào),從函數(shù) CFURLGetPortNumber 獲取。
  • protocol

    • 表示協(xié)議類型的字符串,例如 HTTP 或 HTTPS 。協(xié)議類型通過調(diào)用 CFURLCopyScheme 函數(shù)獲得。
  • authenticationType

    • 認(rèn)證類型,從函數(shù)CFHTTPAuthenticationCopyMethod獲得。
  • passwordLength

    • 0,因?yàn)樵讷@取鑰匙串條目時(shí)不需要密碼。
  • passwordData

    • NULL,因?yàn)樵讷@取鑰匙串條目時(shí)不需要密碼。
  • itemRef

    • 鑰匙串項(xiàng)目引用對(duì)象,SecKeychainItemRef在找到正確的鑰匙串條目后返回。

正確調(diào)用時(shí),代碼應(yīng)該如代碼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);

假設(shè)SecKeychainFindInternetPassword成功返回,請(qǐng)創(chuàng)建包含單個(gè)鑰匙串屬性(SecKeychainAttribute)的鑰匙串屬性列表(SecKeychainAttributeList)。 鑰匙串屬性列表將包含用戶名和密碼。 要加載Keychain屬性列表,請(qǐng)調(diào)用函數(shù)SecKeychainItemCopyContent并將它傳遞給由SecKeychainFindInternetPassword返回的Keychain項(xiàng)目引用對(duì)象(itemRef)。 這個(gè)函數(shù)將用賬號(hào)的用戶名填充keychain屬性,并將一個(gè)void **作為它的密碼。

然后可以使用用戶名和密碼來創(chuàng)建一組新的憑證。 Listing 4-8顯示了這個(gè)過程。

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

從鑰匙串中檢索證書只有在您可以將證書首先存儲(chǔ)在鑰匙串中時(shí)才有用。 這些步驟與加載憑證非常相似。 首先,看看證書是否已存儲(chǔ)在鑰匙串中。 調(diào)用SecKeychainFindInternetPassword,但傳遞accountName的用戶名和accountNameLengthaccountName的長度。

如果條目存在,請(qǐng)修改它以更改密碼。 將Keychain屬性的data字段設(shè)置為包含用戶名,以便修改正確的屬性。 然后調(diào)用函數(shù)SecKeychainItemModifyContent并傳遞鑰匙串項(xiàng)目引用對(duì)象(itemRef),鑰匙串屬性列表和新密碼。 通過修改鑰匙串條目而不是覆蓋它,鑰匙鏈條目將被正確更新,并且任何關(guān)聯(lián)的元數(shù)據(jù)仍將保留。 該條目應(yīng)該與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);

如果條目不存在,那么您將需要從頭創(chuàng)建它。 SecKeychainAddInternetPassword函數(shù)完成此任務(wù)。 其參數(shù)與SecKeychainFindInternetPassword相同,但與對(duì)SecKeychainFindInternetPassword的調(diào)用相比,您提供了SecKeychainAddInternetPassword用戶名和密碼。 在成功調(diào)用SecKeychainAddInternetPassword后釋放鑰匙串項(xiàng)目引用對(duì)象,除非您需要將其用于其他內(nèi)容。 請(qǐng)看Listing 4-10中的函數(shù)調(diào)用。

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 - 認(rèn)證防火墻

對(duì)防火墻進(jìn)行身份驗(yàn)證與驗(yàn)證服務(wù)器非常相似,但必須檢查每個(gè)失敗的HTTP請(qǐng)求以進(jìn)行代理身份驗(yàn)證和服務(wù)器身份驗(yàn)證。這意味著您需要為代理服務(wù)器和原始服務(wù)器分別存儲(chǔ)(本地和永久)存儲(chǔ)。因此,失敗的HTTP響應(yīng)的過程現(xiàn)在將是:

  • 確定響應(yīng)的狀態(tài)代碼是否是407(代理挑戰(zhàn))。如果是,則通過檢查本地代理存儲(chǔ)和持久代理存儲(chǔ)找到匹配的認(rèn)證對(duì)象和憑證。如果這兩者都沒有匹配的對(duì)象和憑證,則請(qǐng)求用戶的憑證。將認(rèn)證對(duì)象應(yīng)用于HTTP請(qǐng)求并重試。
  • 確定響應(yīng)的狀態(tài)代碼是否是401(服務(wù)器質(zhì)詢)。如果是,請(qǐng)按照與407響應(yīng)相同的步驟操作,但使用原始服務(wù)器存儲(chǔ)。
    使用代理服務(wù)器時(shí)還需要??執(zhí)行一些細(xì)微的差異。首先是鑰匙串調(diào)用的參數(shù)來自代理主機(jī)和端口,而不是來自源服務(wù)器的URL。第二個(gè)是當(dāng)詢問用戶的用戶名和密碼時(shí),確保提示清楚地說明了密碼的用途。

按照這些說明,您的應(yīng)用程序應(yīng)該能夠使用認(rèn)證防火墻。

后記

本篇主要講述了與驗(yàn)證HTTP服務(wù)器通信,感興趣的給個(gè)贊或者關(guān)注~~~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容