翻譯:CFNetwork編程指南(四)——與身份驗證HTTP服務器通信(Communicating with Authenticating HTTP Servers)

本文描述了如何利用CFHTTPAuthentication API與需要身份驗證的HTTP服務器通信。它解釋了如何找到匹配的驗證對象和證書,并將它們應用到HTTP請求,然后存儲以供以后使用。

一般來說,如果一個eHTTP服務器返回一個401或407響應你的HTTP請求,這表明服務器進行身份驗證需要證書。在CFHTTPAuthentication API中,每個證書組存儲在CFHTTPAuthentication 對象中。因此,每個不同的身份認證服務器和每個不同用戶連接的服務器需要一個單獨的CFHTTPAuthentication 對象。與服務器通信,你需要應用CFHTTPAuthentication 對象到HTTP請求。接下來更加詳細的解釋這些步驟。

處理身份驗證

添加身份驗證支持將允許你的應用和身份驗證服務器(如果服務器返回401或407響應)進行交互。盡管HTTP身份驗證不是一個難的概念,它是一個復雜的過程。步驟如下:

1.客戶端向服務器發送一個HTTP請求。

2.服務器返回一個驗證給客戶端。

3.客戶端將原始請求的證書打包并發送給服務器。

4.在客戶端和服務器之間談判

5.當服務器驗證了客戶端身份,返回請求的響應。

執行這個過程需要多個步驟。整個過程如圖4-1和4-2.

圖4-1 處理身份驗證
圖4-2 找到一個身份驗證對象

當一個HTTP請求返回一個401或407響應,第一步是為客戶端找到一個有效的CFHTTPAuthentication 對象。一個身份驗證對象包括證書和其他信息,當應用到HTTP消息請求,與服務器驗證你的身份。如果你已經與服務器進行過身份驗證,你會有一個有效的身份驗證對象。然而,在大多數情況下,你需要使用CFHTTPAuthenticationCreateFromResponse 函數來創建一個對象。見列表4-1.

注意:所有關于身份驗證的示例代碼改編自ImageClient 應用。

列表4-1 創建一個身份驗證對象

<pre><code>
if (!authentication) {

CFHTTPMessageRef responseHeader =
    (CFHTTPMessageRef) CFReadStreamCopyProperty(
        readStream,
        kCFStreamPropertyHTTPResponseHeader
    );

// Get the authentication information from the response.
authentication =     

CFHTTPAuthenticationCreateFromResponse(NULL, responseHeader);

CFRelease(responseHeader);

}
</pre></code>

如果新身份驗證對象有效,那么你已經完成可以繼續圖4-1的第二步。如果身份驗證對象無效,然后扔掉身份驗證對象和證書,檢查證書。關于證書的更多信息,閱讀安全證書(Security Credentials)。

不好的證書意味著服務器不接受登陸信息,它將繼續監聽新的證書。然而,如果證書是好的,但服務器仍然拒絕你的請求,然后服務器拒絕與你通信,你必須放棄。加上證書是不好的,重試整個過程,先創建身份驗證對象直到你得到有效的證書和有效的驗證對象。這個過程類似于列表4-2中的代碼。

列表4-2 查找一個有效的身份驗證對象

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

}
</pre></code>

現在你有一個有效的身份驗證對象,繼續圖4-1中的流程。首先,考慮你是否需要證書。如果你不需要,則應由身份驗證對象到HTTP請求。身份驗證對象應用到HTTP請求詳見列表4-4(resumeWithCredentials)。

未存儲證書(在內存中保存證書(Keeping Credentials in Memory )和在永久性倉庫中存儲證書(Keeping Credentials in a Persistent Store)中有解釋),獲取有效證書的唯一方法是提示用戶。大多數情況下,證書需要用戶名和密碼。通過傳遞身份驗證對象到CFHTTPAuthenticationRequiresUserNameAndPassword 函數,你可以看到用戶名和密碼是必須的。如果證書需要用戶名和密碼,提示用戶輸入用戶名和密碼并在證書字典里存儲。對于一個NTLM服務器,證書還需要一個域。在你有新的證書后,你可以調用列表4-4的函數resumeWithCredentials ,應用身份驗證對象到HTTP請求。整個過程見列表4-3。

注意:在代碼列表中,前面有省略號的注釋表明這個功能超出了本文的范圍,但是需要實現。這不同與正常的注釋描述正在發生什么功能。

列表4-3 查找證書(如果需要)并應用它們

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

}
</pre></code>

列表4-4 應用身份驗證對象到請求

<pre><code>
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();
}

}

</pre></code>

在內存中存儲證書

如果你打算經常與一個身份驗證服務器進行通信,重用證書可以來避免多次提示用戶服務器用戶名和密碼。本章解釋了一次性使用身份驗證代碼(例如處理身份驗證(Handling Authentication))需要作出的變更,在內存中存儲證書以便重用。

重用證書,你的代碼中需要更改三個數據結構。

1.創建一個可變的數組來保存所有的身份驗證對象。
<pre><code>CFMutableArrayRef authArray;</pre></code>

代替:
<pre><code>CFHTTPAuthenticationRef authentication;</pre></code>

2.使用字典,創建身份驗證對象到證書的映射。
<pre><code>CFMutableDictionaryRef credentialsDict;</pre></code>

代替:
<pre><code>CFMutableDictionaryRef credentials;</pre></code>

3.保持這些結構在你原來修改當前身份驗證對象和當前證書的地方。
<pre><code>CFDictionaryRemoveValue(credentialsDict, authentication);</pre></code>

代替:
<pre><code>CFRelease(credentials);</pre></code>

現在,創建HTTP請求后,在每次加載前,查找一個匹配的身份驗證對象。查找適合對象的一個簡單的非優化方法見列表4-5.

列表4-5 查找一個匹配的身份驗證對象

<pre><code>
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;

}
</pre></code>

如果身份驗證數組有一個匹配的身份驗證對象,然后檢查證書倉庫是否有正確的證書可用。這樣做可以防止你需要再次提示用戶輸入用戶名和密碼。調用CFDictionaryGetValue 函數可以查找證書,如列表4-6所示。

列表4-6 搜索證書倉庫
<pre><code>credentials = CFDictionaryGetValue(credentialsDict, authentication);</pre></code>

然后應用你的匹配的身份驗證對象和證書到你原始的HTTP請求并重新發送。

警告:在接收到服務器驗證前,不要應用證書到HTTP請求。在你上次認證后,服務器可能改變,你可能會有一個安全風險。

有了這些變更,你的應用可以在內存中存儲身份驗證對象和證書以便未來使用。

在永久性倉庫中存儲證書

在內存中存儲證書可以防止用戶在特定應用啟動時重新輸入服務器用戶名和密碼。然而,當應用退出,這些證書被釋放。為了避免丟失證書,將它們保存到永久性倉庫,這樣每個服務器證書只需要生成一次。推薦用鑰匙鏈來存儲證書。即使你有很多個鑰匙鏈,本文檔中的鑰匙鏈指的是用戶默認的鑰匙鏈。使用鑰匙鏈表明你存儲的身份驗證信息可以用于其他試圖訪問同一個服務器的應用中,反之亦然。

在鑰匙鏈中存儲和檢索證書需要兩個函數:一個用于查找證書字典用于身份驗證,另一個保存最近請求的證書。本文中這些函數聲明如下:
<pre><code>
CFMutableDictionaryRef findCredentialsForAuthentication(

CFHTTPAuthenticationRef auth);

void saveCredentialsForRequest(void);
</pre></code>

findCredentialsForAuthentication 函數首先檢查內存中的證書字典本地緩存是否有證書。如何實現見列表4-6。

如果內存中沒有證書的緩存,然后搜索鑰匙鏈。使用SecKeychainFindInternetPassword函數搜索鑰匙鏈。該函數需要大量的參數。參數和一段簡短的描述HTTP身份驗證證書如何使用它們,如下:

keychainOrArray

NULL 指定用戶默認鑰匙鏈列表。

serverNameLength

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

serverName

從HTTP請求解析到的服務器名稱

securityDomainLength

安全域的長度,或0表示沒有域。在示例代碼中, realm ? strlen(realm) : 0向賬戶傳遞兩種情形。

securityDomain

利用CFHTTPAuthenticationCopyRealm 函數獲取身份驗證對象范圍

accountNameLength

accountName的長度。由于accountNameNULL,值為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

當正確的調用,代碼如列表4-7所示。

列表4-7 搜索鑰匙鏈

<pre><code>
didFind =

SecKeychainFindInternetPassword(NULL,
                                strlen(host), host,
                                realm ? strlen(realm) : 0, realm,
                                0, NULL,
                                path ? strlen(path) : 0, path,
                                port,
                                protocolType,
                                authenticationType,
                                0, NULL,
                                &itemRef);

</pre></code>

假設SecKeychainFindInternetPassword 成功返回,創建一個包含單獨鑰匙鏈屬性(SecKeychainAttribute)的鑰匙鏈屬性列表(SecKeychainAttributeList)。鑰匙鏈實現列表將包含用戶名和密碼。為了加載鑰匙鏈屬性列表,調用SecKeychainItemCopyContent 函數并將SecKeychainFindInternetPassword返回的鑰匙鏈記錄引用對象(itemRef)傳遞給它。該函數將用賬號的用戶名和密碼void \**填充到鑰匙鏈屬性中。

用戶名和密碼可以用來創建一組新證書。列表4-8展示了這個過程。

列表4-8 從鑰匙鏈價值服務器證書。

<pre><code>
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);

}
</pre></code>

如果你可以先存儲證書到鑰匙鏈中,從鑰匙鏈中檢索證書才有用。首先,查看證書是否已經存儲在鑰匙鏈中。調用SecKeychainFindInternetPassword,傳遞用戶名到accountName ,傳遞accountName 的長度到accountNameLength``。

如果記錄存在,修改它來改變密碼。設置鑰匙鏈屬性的數據字段包含用戶名,主要你可以修改正確的屬性。然后調用SecKeychainItemModifyContent 函數并傳遞鑰匙鏈記錄引用對象(itemRef),鑰匙鏈屬性列表和新密碼。通過修改鑰匙鏈記錄而非重寫,鑰匙鏈記錄會正確的更新其他相關數據也將保留。記錄如列表4-9所示。

列表4-9 修改鑰匙鏈記錄
<pre><code>
// 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);
</pre></code>

如果記錄不存在,你將需要從頭開始創建它。SecKeychainAddInternetPassword 函數完成該任務。它的參數與SecKeychainFindInternetPassword相同,但與調用SecKeychainFindInternetPassword相比,你提供用戶名和密碼給SecKeychainAddInternetPassword 。釋放鑰匙鏈記錄引用對象成功后調用SecKeychainAddInternetPassword ,除非你需要在其他地方使用。見列表4-10函數調用。

列表4-10 存儲一個新的鑰匙鏈記錄

<pre><code>
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);

</pre></code>

身份驗證防火墻

身份驗證防火墻與身份驗證服務器非常相似,處理必須檢查每個失敗的HTTP請求的代理身份驗證和服務器身份驗證。這以為著,你需要單獨存儲(本地和永久)代理服務器和源服務器。因此,失敗的HTTP響應的過程如下:

  • 確定響應的狀態碼是否為407(代理懷疑)。如果是,檢查當地代理倉庫和永久性代理倉庫查找一個匹配的身份驗證對象和證書。如果這些都沒有一個匹配的對象和證書,然后請求用戶證書。應用身份驗證對象到HTTP請求并重試。

  • 確定響應的狀態碼是否為401(服務器懷疑)。如果是,遵循與407響應相同的過程,但是用原始服務器存儲。

使用代理服務器有些細微的差別。首先,鑰匙鏈調用的參數來自于代理主機和端口,而非一個源服務器的URL。第二,當要求用戶輸入用戶名和密碼,確保清楚的提示是什么密碼。

通過這些指令,你的應用應該可以使用身份驗證防火墻。

官方原文地址:

https://developer.apple.com/library/ios/documentation/Networking/Conceptual/CFNetwork/CFHTTPAuthenticationTasks/CFHTTPAuthenticationTasks.html#//apple_ref/doc/uid/TP30001132-CH8-SW1

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容