本文描述了如何利用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.
當一個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
的長度。由于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
。
當正確的調用,代碼如列表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。第二,當要求用戶輸入用戶名和密碼,確保清楚的提示是什么密碼。
通過這些指令,你的應用應該可以使用身份驗證防火墻。