本文描述了如何利用CFHTTPAuthentication API與需要身份驗(yàn)證的HTTP服務(wù)器通信烤黍。它解釋了如何找到匹配的驗(yàn)證對(duì)象和證書知市,并將它們應(yīng)用到HTTP請(qǐng)求傻盟,然后存儲(chǔ)以供以后使用。
一般來(lái)說嫂丙,如果一個(gè)eHTTP服務(wù)器返回一個(gè)401或407響應(yīng)你的HTTP請(qǐng)求娘赴,這表明服務(wù)器進(jìn)行身份驗(yàn)證需要證書。在CFHTTPAuthentication API中跟啤,每個(gè)證書組存儲(chǔ)在CFHTTPAuthentication 對(duì)象中诽表。因此,每個(gè)不同的身份認(rèn)證服務(wù)器和每個(gè)不同用戶連接的服務(wù)器需要一個(gè)單獨(dú)的CFHTTPAuthentication 對(duì)象隅肥。與服務(wù)器通信竿奏,你需要應(yīng)用CFHTTPAuthentication 對(duì)象到HTTP請(qǐng)求。接下來(lái)更加詳細(xì)的解釋這些步驟腥放。
處理身份驗(yàn)證
添加身份驗(yàn)證支持將允許你的應(yīng)用和身份驗(yàn)證服務(wù)器(如果服務(wù)器返回401或407響應(yīng))進(jìn)行交互泛啸。盡管HTTP身份驗(yàn)證不是一個(gè)難的概念,它是一個(gè)復(fù)雜的過程秃症。步驟如下:
1.客戶端向服務(wù)器發(fā)送一個(gè)HTTP請(qǐng)求候址。
2.服務(wù)器返回一個(gè)驗(yàn)證給客戶端。
3.客戶端將原始請(qǐng)求的證書打包并發(fā)送給服務(wù)器种柑。
4.在客戶端和服務(wù)器之間談判
5.當(dāng)服務(wù)器驗(yàn)證了客戶端身份岗仑,返回請(qǐng)求的響應(yīng)。
執(zhí)行這個(gè)過程需要多個(gè)步驟聚请。整個(gè)過程如圖4-1和4-2.
當(dāng)一個(gè)HTTP請(qǐng)求返回一個(gè)401或407響應(yīng)赔蒲,第一步是為客戶端找到一個(gè)有效的CFHTTPAuthentication 對(duì)象。一個(gè)身份驗(yàn)證對(duì)象包括證書和其他信息良漱,當(dāng)應(yīng)用到HTTP消息請(qǐng)求舞虱,與服務(wù)器驗(yàn)證你的身份。如果你已經(jīng)與服務(wù)器進(jìn)行過身份驗(yàn)證母市,你會(huì)有一個(gè)有效的身份驗(yàn)證對(duì)象矾兜。然而,在大多數(shù)情況下患久,你需要使用CFHTTPAuthenticationCreateFromResponse 函數(shù)來(lái)創(chuàng)建一個(gè)對(duì)象椅寺。見列表4-1.
注意:所有關(guān)于身份驗(yàn)證的示例代碼改編自ImageClient 應(yīng)用。
列表4-1 創(chuàng)建一個(gè)身份驗(yàn)證對(duì)象
<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>
如果新身份驗(yàn)證對(duì)象有效蒋失,那么你已經(jīng)完成可以繼續(xù)圖4-1的第二步返帕。如果身份驗(yàn)證對(duì)象無(wú)效,然后扔掉身份驗(yàn)證對(duì)象和證書篙挽,檢查證書荆萤。關(guān)于證書的更多信息,閱讀安全證書(Security Credentials)。
不好的證書意味著服務(wù)器不接受登陸信息链韭,它將繼續(xù)監(jiān)聽新的證書偏竟。然而,如果證書是好的敞峭,但服務(wù)器仍然拒絕你的請(qǐng)求踊谋,然后服務(wù)器拒絕與你通信,你必須放棄旋讹。加上證書是不好的殖蚕,重試整個(gè)過程,先創(chuàng)建身份驗(yàn)證對(duì)象直到你得到有效的證書和有效的驗(yàn)證對(duì)象沉迹。這個(gè)過程類似于列表4-2中的代碼嫌褪。
列表4-2 查找一個(gè)有效的身份驗(yàn)證對(duì)象
<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>
現(xiàn)在你有一個(gè)有效的身份驗(yàn)證對(duì)象,繼續(xù)圖4-1中的流程胚股。首先,考慮你是否需要證書裙秋。如果你不需要琅拌,則應(yīng)由身份驗(yàn)證對(duì)象到HTTP請(qǐng)求。身份驗(yàn)證對(duì)象應(yīng)用到HTTP請(qǐng)求詳見列表4-4(resumeWithCredentials
)摘刑。
未存儲(chǔ)證書(在內(nèi)存中保存證書(Keeping Credentials in Memory )和在永久性倉(cāng)庫(kù)中存儲(chǔ)證書(Keeping Credentials in a Persistent Store)中有解釋)进宝,獲取有效證書的唯一方法是提示用戶。大多數(shù)情況下枷恕,證書需要用戶名和密碼党晋。通過傳遞身份驗(yàn)證對(duì)象到CFHTTPAuthenticationRequiresUserNameAndPassword 函數(shù),你可以看到用戶名和密碼是必須的徐块。如果證書需要用戶名和密碼未玻,提示用戶輸入用戶名和密碼并在證書字典里存儲(chǔ)。對(duì)于一個(gè)NTLM服務(wù)器胡控,證書還需要一個(gè)域扳剿。在你有新的證書后,你可以調(diào)用列表4-4的函數(shù)resumeWithCredentials
昼激,應(yīng)用身份驗(yàn)證對(duì)象到HTTP請(qǐng)求庇绽。整個(gè)過程見列表4-3。
注意:在代碼列表中橙困,前面有省略號(hào)的注釋表明這個(gè)功能超出了本文的范圍瞧掺,但是需要實(shí)現(xiàn)。這不同與正常的注釋描述正在發(fā)生什么功能凡傅。
列表4-3 查找證書(如果需要)并應(yīng)用它們
<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 應(yīng)用身份驗(yàn)證對(duì)象到請(qǐng)求
<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>
在內(nèi)存中存儲(chǔ)證書
如果你打算經(jīng)常與一個(gè)身份驗(yàn)證服務(wù)器進(jìn)行通信辟狈,重用證書可以來(lái)避免多次提示用戶服務(wù)器用戶名和密碼。本章解釋了一次性使用身份驗(yàn)證代碼(例如處理身份驗(yàn)證(Handling Authentication))需要作出的變更夏跷,在內(nèi)存中存儲(chǔ)證書以便重用上陕。
重用證書桩砰,你的代碼中需要更改三個(gè)數(shù)據(jù)結(jié)構(gòu)。
1.創(chuàng)建一個(gè)可變的數(shù)組來(lái)保存所有的身份驗(yàn)證對(duì)象释簿。
<pre><code>CFMutableArrayRef authArray;</pre></code>
代替:
<pre><code>CFHTTPAuthenticationRef authentication;</pre></code>
2.使用字典亚隅,創(chuàng)建身份驗(yàn)證對(duì)象到證書的映射。
<pre><code>CFMutableDictionaryRef credentialsDict;</pre></code>
代替:
<pre><code>CFMutableDictionaryRef credentials;</pre></code>
3.保持這些結(jié)構(gòu)在你原來(lái)修改當(dāng)前身份驗(yàn)證對(duì)象和當(dāng)前證書的地方庶溶。
<pre><code>CFDictionaryRemoveValue(credentialsDict, authentication);</pre></code>
代替:
<pre><code>CFRelease(credentials);</pre></code>
現(xiàn)在煮纵,創(chuàng)建HTTP請(qǐng)求后,在每次加載前偏螺,查找一個(gè)匹配的身份驗(yàn)證對(duì)象行疏。查找適合對(duì)象的一個(gè)簡(jiǎn)單的非優(yōu)化方法見列表4-5.
列表4-5 查找一個(gè)匹配的身份驗(yàn)證對(duì)象
<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>
如果身份驗(yàn)證數(shù)組有一個(gè)匹配的身份驗(yàn)證對(duì)象,然后檢查證書倉(cāng)庫(kù)是否有正確的證書可用套像。這樣做可以防止你需要再次提示用戶輸入用戶名和密碼酿联。調(diào)用CFDictionaryGetValue 函數(shù)可以查找證書,如列表4-6所示夺巩。
列表4-6 搜索證書倉(cāng)庫(kù)
<pre><code>credentials = CFDictionaryGetValue(credentialsDict, authentication);</pre></code>
然后應(yīng)用你的匹配的身份驗(yàn)證對(duì)象和證書到你原始的HTTP請(qǐng)求并重新發(fā)送贞让。
警告:在接收到服務(wù)器驗(yàn)證前,不要應(yīng)用證書到HTTP請(qǐng)求柳譬。在你上次認(rèn)證后喳张,服務(wù)器可能改變,你可能會(huì)有一個(gè)安全風(fēng)險(xiǎn)美澳。
有了這些變更销部,你的應(yīng)用可以在內(nèi)存中存儲(chǔ)身份驗(yàn)證對(duì)象和證書以便未來(lái)使用。
在永久性倉(cāng)庫(kù)中存儲(chǔ)證書
在內(nèi)存中存儲(chǔ)證書可以防止用戶在特定應(yīng)用啟動(dòng)時(shí)重新輸入服務(wù)器用戶名和密碼制跟。然而舅桩,當(dāng)應(yīng)用退出,這些證書被釋放雨膨。為了避免丟失證書江咳,將它們保存到永久性倉(cāng)庫(kù),這樣每個(gè)服務(wù)器證書只需要生成一次哥放。推薦用鑰匙鏈來(lái)存儲(chǔ)證書歼指。即使你有很多個(gè)鑰匙鏈,本文檔中的鑰匙鏈指的是用戶默認(rèn)的鑰匙鏈甥雕。使用鑰匙鏈表明你存儲(chǔ)的身份驗(yàn)證信息可以用于其他試圖訪問同一個(gè)服務(wù)器的應(yīng)用中踩身,反之亦然。
在鑰匙鏈中存儲(chǔ)和檢索證書需要兩個(gè)函數(shù):一個(gè)用于查找證書字典用于身份驗(yàn)證社露,另一個(gè)保存最近請(qǐng)求的證書挟阻。本文中這些函數(shù)聲明如下:
<pre><code>
CFMutableDictionaryRef findCredentialsForAuthentication(
CFHTTPAuthenticationRef auth);
void saveCredentialsForRequest(void);
</pre></code>
findCredentialsForAuthentication
函數(shù)首先檢查內(nèi)存中的證書字典本地緩存是否有證書。如何實(shí)現(xiàn)見列表4-6。
如果內(nèi)存中沒有證書的緩存附鸽,然后搜索鑰匙鏈脱拼。使用SecKeychainFindInternetPassword
函數(shù)搜索鑰匙鏈。該函數(shù)需要大量的參數(shù)坷备。參數(shù)和一段簡(jiǎn)短的描述HTTP身份驗(yàn)證證書如何使用它們熄浓,如下:
keychainOrArray
NULL
指定用戶默認(rèn)鑰匙鏈列表。
serverNameLength
serverName
的長(zhǎng)度省撑,通常是strlen(serverName)``赌蔑。
serverName
從HTTP請(qǐng)求解析到的服務(wù)器名稱
securityDomainLength
安全域的長(zhǎng)度,或0表示沒有域竟秫。在示例代碼中娃惯, realm ? strlen(realm) : 0
向賬戶傳遞兩種情形。
securityDomain
利用CFHTTPAuthenticationCopyRealm 函數(shù)獲取身份驗(yàn)證對(duì)象范圍
accountNameLength
accountName
的長(zhǎng)度肥败。由于accountName
是NULL
趾浅,值為0
accountName
當(dāng)讀取鑰匙鏈記錄時(shí)沒有賬戶名,該字段為NULL
馒稍。
pathLength
path
的長(zhǎng)度皿哨,如果沒有路徑則為0.在示例代碼中,path ? strlen(path) : 0
向賬戶傳遞兩種情形筷黔。
path
利用CFURLCopyPath 函數(shù)從身份驗(yàn)證對(duì)象獲取路徑。
port
利用CFURLGetPortNumber
函數(shù)獲取端口號(hào)仗颈。
protocol
代表協(xié)議類型的字符串佛舱,例如HTTP或HTTPS。通過CFURLCopyScheme
函數(shù)獲取協(xié)議類型挨决。
authenticationType
利用CFHTTPAuthenticationCopyMethod
函數(shù)獲取身份驗(yàn)證類型请祖。
passwordLength
0,因?yàn)樵谧x取鑰匙鏈記錄時(shí)不需要密碼脖祈。
passwordData
NULL
肆捕,因?yàn)樵谧x取鑰匙鏈記錄時(shí)不需要密碼。
itemRef
查找到正確的鑰匙鏈記錄盖高,返回鑰匙鏈記錄引用對(duì)象SecKeychainItemRef
慎陵。
當(dāng)正確的調(diào)用,代碼如列表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>
假設(shè)SecKeychainFindInternetPassword
成功返回席纽,創(chuàng)建一個(gè)包含單獨(dú)鑰匙鏈屬性(SecKeychainAttribute
)的鑰匙鏈屬性列表(SecKeychainAttributeList
)。鑰匙鏈實(shí)現(xiàn)列表將包含用戶名和密碼撞蚕。為了加載鑰匙鏈屬性列表润梯,調(diào)用SecKeychainItemCopyContent
函數(shù)并將SecKeychainFindInternetPassword
返回的鑰匙鏈記錄引用對(duì)象(itemRef
)傳遞給它。該函數(shù)將用賬號(hào)的用戶名和密碼void \**
填充到鑰匙鏈屬性中。
用戶名和密碼可以用來(lái)創(chuàng)建一組新證書纺铭。列表4-8展示了這個(gè)過程寇钉。
列表4-8 從鑰匙鏈價(jià)值服務(wù)器證書。
<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>
如果你可以先存儲(chǔ)證書到鑰匙鏈中舶赔,從鑰匙鏈中檢索證書才有用扫倡。首先,查看證書是否已經(jīng)存儲(chǔ)在鑰匙鏈中顿痪。調(diào)用SecKeychainFindInternetPassword
镊辕,傳遞用戶名到accountName
,傳遞accountName
的長(zhǎng)度到accountNameLength``蚁袭。
如果記錄存在征懈,修改它來(lái)改變密碼。設(shè)置鑰匙鏈屬性的數(shù)據(jù)字段包含用戶名揩悄,主要你可以修改正確的屬性卖哎。然后調(diào)用SecKeychainItemModifyContent
函數(shù)并傳遞鑰匙鏈記錄引用對(duì)象(itemRef
),鑰匙鏈屬性列表和新密碼删性。通過修改鑰匙鏈記錄而非重寫亏娜,鑰匙鏈記錄會(huì)正確的更新其他相關(guān)數(shù)據(jù)也將保留。記錄如列表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>
如果記錄不存在维贺,你將需要從頭開始創(chuàng)建它。SecKeychainAddInternetPassword
函數(shù)完成該任務(wù)巴帮。它的參數(shù)與SecKeychainFindInternetPassword
相同溯泣,但與調(diào)用SecKeychainFindInternetPassword
相比,你提供用戶名和密碼給SecKeychainAddInternetPassword
榕茧。釋放鑰匙鏈記錄引用對(duì)象成功后調(diào)用SecKeychainAddInternetPassword
垃沦,除非你需要在其他地方使用。見列表4-10函數(shù)調(diào)用用押。
列表4-10 存儲(chǔ)一個(gè)新的鑰匙鏈記錄
<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>
身份驗(yàn)證防火墻
身份驗(yàn)證防火墻與身份驗(yàn)證服務(wù)器非常相似肢簿,處理必須檢查每個(gè)失敗的HTTP請(qǐng)求的代理身份驗(yàn)證和服務(wù)器身份驗(yàn)證。這以為著蜻拨,你需要單獨(dú)存儲(chǔ)(本地和永久)代理服務(wù)器和源服務(wù)器池充。因此,失敗的HTTP響應(yīng)的過程如下:
確定響應(yīng)的狀態(tài)碼是否為407(代理懷疑)缎讼。如果是纵菌,檢查當(dāng)?shù)卮韨}(cāng)庫(kù)和永久性代理倉(cāng)庫(kù)查找一個(gè)匹配的身份驗(yàn)證對(duì)象和證書。如果這些都沒有一個(gè)匹配的對(duì)象和證書休涤,然后請(qǐng)求用戶證書咱圆。應(yīng)用身份驗(yàn)證對(duì)象到HTTP請(qǐng)求并重試笛辟。
確定響應(yīng)的狀態(tài)碼是否為401(服務(wù)器懷疑)。如果是序苏,遵循與407響應(yīng)相同的過程手幢,但是用原始服務(wù)器存儲(chǔ)。
使用代理服務(wù)器有些細(xì)微的差別忱详。首先围来,鑰匙鏈調(diào)用的參數(shù)來(lái)自于代理主機(jī)和端口,而非一個(gè)源服務(wù)器的URL匈睁。第二监透,當(dāng)要求用戶輸入用戶名和密碼,確保清楚的提示是什么密碼航唆。
通過這些指令胀蛮,你的應(yīng)用應(yīng)該可以使用身份驗(yàn)證防火墻。