iOS Communicating with Authenticating HTTP Servers 與HTTP伺服器通訊認證 官方文檔翻譯(六)
與HTTP伺服器通訊認證
本章描述了如何與HTTP伺服器身分識別驗證利用CFHTTPAuthentication API。它解釋了如何找到匹配驗證對象和憑證,將它們應用到一個HTTP請求,並將它們儲存供以後使用。
一般來說,如果一個HTTP伺服器將返回一個401或407響應後你的HTTP請求,這意味著伺服器進行身分識別驗證,需要憑證。在CFHTTPAuthentication API中,每個組憑證儲存在一個CFHTTPAuthentication對象。因此,每一個不同的伺服器和每個不同的使用者串連到伺服器進行身分識別驗證需要一個單獨的CFHTTPAuthentication對象。與伺服器通訊,需要CFHTTPAuthentication對象應用於HTTP請求。更詳細地解釋了這些步驟。
處理身分識別驗證
添加支援身分識別驗證將允許您的應用程式和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。
注意:所有的範例程式碼關於身分識別驗證是改編自ImageClient應用程式。
清單4建立身分識別驗證對象
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”。
壞憑證意味著伺服器不接受登入資訊,它將繼續監聽新的憑證。然而,如果伺服器憑證是好的但是仍然拒絕了你的請求,然後伺服器拒絕和你說話,所以你必須放棄。假設憑證是壞,重試整個過程開始建立身分識別驗證對象,直到你得到工作憑證和有效驗證對象。在代碼中,這個過程應該類似於清單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);
}
}
清單4 - 4應用請求的身分識別驗證對象
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();
}
}
在記憶體中保留憑證
如果你打算經常與一個證明伺服器交流,值得重用憑證來避免提示使用者多次伺服器的使用者名稱和密碼。本節解釋的變化應一次性使用身分識別驗證代碼(比如在處理身分識別驗證)來儲存憑證在記憶體中以便重用。
重用憑證,有三個資料結構更改你需要讓你的代碼。
1.建立一個可變的數組來儲存所有身分識別驗證對象。
CFMutableArrayRef authArray;
而不是:
CFHTTPAuthenticationRef authentication;
2.建立一個從身分識別驗證對象映射到憑證使用字典。
CFMutableDictionaryRef credentialsDict;
而不是:
CFMutableDictionaryRef credentials;
3.保持這些結構都用來修改當前驗證對象和當前憑證。
CFDictionaryRemoveValue(credentialsDict, authentication);
而不是:
CFRelease(credentials);
現在,建立HTTP請求後,尋找一個匹配的驗證對象之前每個負載。一個簡單的、非最佳化方法尋找合適的對象可以是清單4 - 5所示。
清單4 - 5尋找一個匹配的驗證對象
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函數如清單4 - 6所示。
清單4 - 6搜尋憑證儲存
credentials = CFDictionaryGetValue(credentialsDict, authentication);
然後應用匹配驗證對象和憑證原始HTTP請求並重新發送它。
警告:不適用的憑證用於HTTP請求接收伺服器之前的要求。自從你上次認證時伺服器可能改變了,你可以建立一個安全風險。
這些變化,你應用程式將能夠在記憶體中儲存身分識別驗證對象和憑證,以備未來使用。
保持持久性儲存憑證
在記憶體中儲存憑證可以防止使用者不得不重新輸入伺服器的使用者名稱和密碼在這特定的應用程式啟動。然而,當應用程式退出時,這些認證將被釋放。為了避免失去憑據,將它們儲存在一個持久化儲存中,每個伺服器的認證需要只產生一次。鑰匙鏈是推薦的地方用來儲存憑證。即使你可以有多個鑰匙,這個文檔是指使用者的預設密鑰鏈鑰匙扣。使用鑰匙鏈意味著身分識別驗證資訊,您還可以使用儲存在其他應用程式上,試圖訪問同一台伺服器,反之亦然。
儲存和檢索憑證的鑰匙扣需要兩個函數:一個用於尋找憑證字典用於身分識別驗證和一個用於儲存憑證最近的請求。這些函數將本文檔中聲明為:
CFMutableDictionaryRef findCredentialsForAuthentication(
CFHTTPAuthenticationRef auth);
void saveCredentialsForRequest(void);
函數findCredentialsForAuthentication首先檢查憑證字典儲存在記憶體中憑證是否在本機快取。清單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。在樣本代碼中,path ? strlen(path) : 0傳遞到賬戶有兩種情況。。
path
身分識別驗證對象的路徑,從CFURLCopyPath獲得功能。
port
連接埠號碼,從函數CFURLGetPortNumber獲得。
protocol
一個字串代表協議類型,如HTTP或HTTPS。獲得的協議類型是通過調用CFURLCopyScheme功能。
authenticationType
身分識別驗證類型,從函數CFHTTPAuthenticationCopyMethod獲得。
passwordLength
0,因為沒有密碼而擷取密鑰鏈項的時候是必要的。
passwordData
NULL,因為沒有密碼而擷取密鑰鏈項的時候是必要的。
itemRef
SecKeychainItemRef密鑰鏈項引用對象,返回時找到正確的鑰匙鏈條目。
當正確時,應該如清單4所示的代碼。
清單4 - 7搜尋鑰匙鏈
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成功返回,建立一個鑰匙扣屬性列表(SecKeychainAttributeList)包含一個鑰匙扣屬性(SecKeychainAttribute)。鑰匙鏈的屬性列表將包含使用者名稱和密碼。載入密鑰鏈屬性列表,調用函數SecKeychainItemCopyContent並將其傳遞給密鑰鏈項引用對象(itemRef)被SecKeychainFindInternetPassword返回。這個函數將填補鑰匙扣屬性賬戶的使用者名稱,和一個void * *作為密碼
使用者名稱和密碼可以用來建立一個新組憑證。清單4 - 8顯示了這個過程。
清單4 - 8從鑰匙鏈載入伺服器憑證
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,但通過使用者名稱為accountnamelength的帳戶名稱和帳戶名稱的長度。
如果條目存在,修改它改變密碼。鑰匙鏈的資料欄位屬性設定為包含使用者名稱,以便您修改正確的屬性。然後調用函數SecKeychainItemModifyContent並通過密鑰鏈項引用對象(itemRef),鑰匙鏈屬性列表,和新密碼。通過修改密鑰鏈項而不是重寫,鑰匙鏈上的條目將會正確地更新和任何相關的中繼資料仍將保留。入口應該類似於清單4 - 9。
清單4 - 9日修改密鑰鏈項
// 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除非你需要使用別的東西。見清單4到10中的函數調用。
清單4到10儲存一個新的密鑰鏈項
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);
驗證防火牆
驗證防火牆非常類似於驗證服務器除了必須檢查每一個失敗的HTTP請求Proxy 驗證和伺服器身分識別驗證。這意味著你需要單獨的(本地和持久)Proxy 伺服器和原始伺服器。因此,失敗的HTTP響應的過程將會:
確定響應的狀態代碼407(一個代理的要求)。如果是,找到一個匹配驗證對象和憑證通過檢查當地代理儲存和持久代理商店。如果這些都沒有一個匹配的對象和憑證,然後從使用者請求認證。認證對象應用於HTTP請求並再試一次。
確定響應的狀態代碼401(一個伺服器的挑戰)。如果是,遵循相同的過程與一個407響應,但使用原始伺服器的商店。
在使用Proxy 伺服器執行時也有一些細微的差別。首先,鑰匙扣調用的參數來自代理主機和連接埠,而不是從一個原始伺服器的URL。第二個是,當要求使用者輸入使用者名稱和密碼,確保及時明確的密碼是什麼。
通過遵循這些說明,您的應用程式應該能夠處理驗證防火牆。