一、前言
HTTP認證是Web服務(wù)器對客戶(hù)端的權限進(jìn)行認證的一種方式,能夠為Web應用提供一定程度的安全保障。目前一些Web應用項目已經(jīng)提出了采用HTTP認證的需求。雖然一般的Web容器都提供基本認證和摘要認證的API,但不同的Web容器提供的API也互不相同,因此我們在ZX Web平臺的工具包中提供了一組API,利用這組API,開(kāi)發(fā)人員可以在應用程序中使用統一的接口輕松實(shí)現HTTP認證功能,而不必依賴(lài)于Web容器。
二、HTTP認證機制
HTTP認證采用“質(zhì)詢(xún)-響應(challenge-response)”的機制。“質(zhì)詢(xún)”是服務(wù)器端對客戶(hù)端的質(zhì)詢(xún),即要求客戶(hù)端發(fā)送認證信息;“響應”是客戶(hù)端對“質(zhì)詢(xún)”的響應,即發(fā)送帶有認證信息的HTTP請求。
一般來(lái)說(shuō),客戶(hù)端第一次請求一個(gè)URI時(shí),并不知道是否需要認證,因此總是不帶認證信息的,這時(shí)服務(wù)器端就會(huì )找不到認證信息,認證失敗,于是向客戶(hù)端發(fā)出一個(gè)“質(zhì)詢(xún)”。
所謂“發(fā)出質(zhì)詢(xún)”,就是給客戶(hù)端發(fā)送一個(gè)HTTP響應,其狀態(tài)碼為401 (Unauthorized),并且包含消息頭WWW-Authenticate,客戶(hù)端看到這個(gè)響應就知道這個(gè)URI需要認證。WWW-Authenticate消息頭格式為
WWW-Authenticate:challenge
其中<challenge>是就是質(zhì)詢(xún)信息,RFC2617中的定義為:
challenge = auth-scheme 1*SP 1#auth-param
auth-scheme = token
auth-param = token "=" ( token | quoted-string )
在challenge的定義中,首先是auth-scheme,即認證方案,它被定義為一個(gè)token,即預定義的符號。所謂token,就是一些字符串,但這些字符串不是隨意的,而是大家約定的,它們具有特定的含義。auth-scheme的取值只能是Basic或Digest,分別表示基本認證和摘要認證,這兩個(gè)單詞就是token。這里沒(méi)有把auth-scheme定義為Basic|Digest,而是一個(gè)token,說(shuō)明還可以進(jìn)行擴展,還可以取其他符號——只要服務(wù)器端和客戶(hù)端互相約定都能理解就行。
接著(zhù),“1*SP”表示1個(gè)或多個(gè)空格符。其中“1*”表示數量為1個(gè)到多個(gè),“SP”即空格符(ASCII碼32)。
然后是“1#auth-param”,表示一個(gè)auth-param的列表。其中的“1#”也表示后面的元素是1到多個(gè),但與“1*”不同的是,“1#”表示一個(gè)“列表”,即元素之間是用逗號“,”分隔開(kāi)的。列表中的每個(gè)auth-param被定義為一個(gè)名值對,即
符號=符號
或
符號=“引號中的字符串”
這兩種形式。
基本認證和摘要認證中都定義了一個(gè)相同的auth-param,即realm,定義為:
realm = “realm”“=” realm-value
realm-value = quoted-string
realm-value是一個(gè)兩端加引號的大小寫(xiě)相關(guān)的字符串,表示要求認證的“領(lǐng)域(realm)”。領(lǐng)域是由服務(wù)器自己決定的,不同的服務(wù)器可以設置自己的領(lǐng)域,同一個(gè)服務(wù)器也可以有多個(gè)領(lǐng)域。質(zhì)詢(xún)中包含領(lǐng)域信息是為了讓客戶(hù)端知道哪個(gè)范圍的用戶(hù)名是合法的,RFC2617中建議領(lǐng)域至少包含主機名和有權限的用戶(hù)組,例如“registered_users@www.news.com”。
客戶(hù)端收到質(zhì)詢(xún)后,應該給服務(wù)器端返回一個(gè)“響應”,即重新發(fā)送一個(gè)新的HTTP請求。這個(gè)新的HTTP請求與前一個(gè)HTTP請求的差別在于多了一個(gè)Authorization消息頭,該消息頭的格式為Authorization:credentials,其中的credentials就是認證信息,認證信息的格式根據不同的認證方案而有所不同。
服務(wù)器端對認證信息進(jìn)行判斷,只有認證通過(guò),才會(huì )響應客戶(hù)端的請求。
1. 基本認證
基本認證的質(zhì)詢(xún)中只定義了一種auth-param,即realm,因此基本認證的質(zhì)詢(xún)也定義為
challenge = “Basic” realm
質(zhì)詢(xún)舉例:
當服務(wù)器端認證不通過(guò),將返回一個(gè)狀態(tài)碼為401(Unautherized)的響應消息,并帶有如下消息頭:
WWW-Authenticate: Basic realm=“My Secret World”
基本認證的認證信息credentials定義為:
credentials = “Basic” basic-credentials
basic-credentials = base64-user-pass
base64-user-pass = <base64 encoding of user-pass,
except not limited to 76 char/line>
user-pass = userid “:” password
userid = *
password = *TEXT
簡(jiǎn)單說(shuō),認證信息就是“Basic”后面加上“<用戶(hù)名>:<密碼>”的Base64編碼,只不過(guò)這里的Base64編碼不對每一行的字符數做最大76個(gè)的限制。
認證信息舉例:
如果用戶(hù)名為“abc”,密碼為“abcd”,將“abc:abcd”進(jìn)行Base64編碼得到“YWJjOmFiY2Q=”,于是消息頭中認證信息為
Authorization: Basic YWJjOmFiY2Q=
2. 摘要認證
由于基本認證被認為是不安全的認證方式,摘要認證作為替代方案被制定了出來(lái)。摘要認證中,用戶(hù)名和密碼不會(huì )以明文方式傳送,而是經(jīng)過(guò)了加密。從名稱(chēng)可以看出,是生成了信息摘要,客戶(hù)端和服務(wù)器使用各自的密碼以同樣的算法生成信息摘要,兩者比較即可判斷客戶(hù)端的密碼是否正確。
摘要認證仍然采用WWW-Authenticate和Authorization兩個(gè)消息頭,另外還規定了消息頭Authentication-Info。消息頭Authentication-Info用于認證通過(guò)之后,服務(wù)器給客戶(hù)端返回一些信息,例如可以用來(lái)指定下一次認證用的臨時(shí)值,或者也生成一個(gè)摘要,表明服務(wù)器確實(shí)知道用戶(hù)密碼,等等。不過(guò)這個(gè)消息頭并不是必須的,實(shí)際應用中一般也用不著(zhù),因此Web平臺中目前沒(méi)有實(shí)現,這里也不做介紹,若有興趣請查看RFC2617。
三、HTTP認證的安全性
1. 基本認證的安全性
基本認證不是一種安全的認證方式,因為Base64編碼僅僅是編碼,而不是加密,以這種形式在互聯(lián)網(wǎng)上傳遞用戶(hù)名和密碼,其危險性是顯而易見(jiàn)的。但如果對安全性要求不高,則可以使用這種認證方式做為最簡(jiǎn)單的安全措施--畢竟比沒(méi)有安全措施要好。
當然,如果能夠保證中間不會(huì )有人截取數據包,例如處于內部局域網(wǎng),或者底層協(xié)議是安全的(如使用SSL或其他一些安全機制),倒是可以彌補HTTP基本認證在安全性方面的不足。
2. 摘要認證的安全性
由于基本認證過(guò)于危險,人們才使用摘要認證作為一種替代方案。但它也僅僅是作為基本認證的替代品,因為它本身也不是十分安全的,也存在一些弱點(diǎn)。
(1)摘要認證只能作為權限認證機制,并非保密措施,因為消息體并沒(méi)有被加密。qop使用“auth-int”只能保證消息體不被修改,不能防止被偷看。
(2)Replay攻擊:攻擊者可能截取一次摘要信息,然后利用相同的摘要信息請求相同的URI,如果該URI可以通過(guò)POST或PUT方法訪(fǎng)問(wèn),則攻擊者可能修改消息體??刂苙once中的時(shí)間戳和nc次數有助于減小replay攻擊機會(huì );每次使用新的nonce值(用Authentication-Info消息頭)可避免遭受replay攻擊,當然也增加了開(kāi)銷(xiāo)。
(3)MITM(Man in the Middle)攻擊:攻擊者截取網(wǎng)絡(luò )數據包,給客戶(hù)端發(fā)送一個(gè)假的質(zhì)詢(xún),只要求客戶(hù)端使用基本認證,從而取得密碼。MITM最常見(jiàn)的方式是提供一個(gè)“免費”的但其實(shí)是惡意的代理服務(wù)器。要防止此類(lèi)攻擊,可雙方約定只使用摘要認證,不允許使用基本認證,但一般瀏覽器并不支持指定認證方式,除非是自己開(kāi)發(fā)的客戶(hù)端。
其他還有些攻擊方式,例如通過(guò)“查字典”猜密碼等比較野蠻的方式。雖然摘要認證有這些弱點(diǎn),但在許多情況下還是有它的使用價(jià)值的,至少比基本認證是好多了。
四、HTTP認證在ZX Web平臺中的實(shí)現
在ZX Web平臺的包中提供了HTTP認證的API,這組API包括一個(gè)接口HttpAuth以及該接口的兩個(gè)實(shí)現類(lèi)HttpBasicAuth和HttpDigestAuth,分別實(shí)現基本認證和摘要認證。開(kāi)發(fā)人員可以使用接口,也可以直接使用兩個(gè)實(shí)現類(lèi)。類(lèi)圖如圖1所示。
1. HttpAuth接口
這個(gè)接口應該提供一個(gè)方法取得用戶(hù)提交的用戶(hù)名和密碼,以便應用程序校驗其正確性。在基本認證中,這一點(diǎn)是可以做到的,但在摘要認證中服務(wù)器端并不能知道客戶(hù)端提交的密碼,所能得到的只是對包含密碼的數據進(jìn)行MD5編碼所得到一個(gè)摘要,因此為了照顧摘要認證,HttpAuth接口沒(méi)有設計這樣一個(gè)方法,而是提供另一個(gè)方法authenticate,調用者將用戶(hù)名和正確的密碼作為該方法的參數傳入,返回認證結果。該方法還需要另一個(gè)參數,即HTTP請求HttpServletRequest的一個(gè)實(shí)例,因為需要從中取得用戶(hù)提交的認證信息;HttpAuth接口的所有方法都需要這個(gè)參數。
在大多數情況下,合法的用戶(hù)不止一個(gè),因此調用authenticate方法之前必須知道客戶(hù)端提交的用戶(hù)名。HttpAuth接口的getUserName方法完成此功能。這兩個(gè)方法描述如下:
getUserName
功能:
取得用戶(hù)發(fā)來(lái)的認證信息中的用戶(hù)名。
原型:
public String getUserName(HttpServletRequest req)
參數:
req - HTTP請求
返回:
返回認證信息中的用戶(hù)名。若未取到(請求中未包含認證信息或認證信息格式不正確),返回null。
authenticate
功能:
判斷一個(gè)HTTP請求的認證信息中用戶(hù)名和密碼是否與指定的相符。
原型:
public boolean authenticate(HttpServletRequest req,
String userName,
String password)
參數:
req - HTTP請求
userName - 指定的用戶(hù)名
password - 指定的密碼
返回:
true - 認證通過(guò)
false - 認證未通過(guò)
當調用authenticate方法發(fā)現認證未通過(guò),應該給客戶(hù)端發(fā)回一個(gè)質(zhì)詢(xún),即設置響應的狀態(tài)碼為401,并設置消息頭WWW-Authenticate。HttpAuth接口的setUnauth方法負責完成此功能。
SetUnauth
功能:
設置 401 Unauthorized 狀態(tài)碼,并添加WWW-Authenticate消息頭。
原型:
public void setUnauth(HttpServletRequest req,
HttpServletResponse rsp,
String realm)
參數:
req - HTTP請求
rsp - HTTP響應
realm - WWW-Authenticate消息頭中的realm值。若為null,則默認使用 servername:port
返回:無(wú)
setUnauth方法設置HTTP響應對象rsp的狀態(tài)碼為401,并添加WWW-Authenticate消息頭,至于消息頭的內容,HttpBasicAuth類(lèi)和HttpDigestAuth類(lèi)根據基本認證和摘要認證的規范不同而有不同的實(shí)現。設置WWW-Authenticate消息頭需要realm值,調用者通過(guò)realm參數指定realm值。調用者如果不想指定realm值,可以置realm參數為null,或直接調用另一種參數形式的setUnauth方法:
public void setUnauth(HttpServletRequest req, HttpServletResponse rsp)
如果不指定realm值,setUnauth方法將從請求對象req中取得服務(wù)器名和端口,按servername:port組合,以此作為realm的值。
最后,HttpAuth接口提供getAuth方法,以取得HTTP請求中用戶(hù)提交的認證信息,即Authorization消息頭中"Basic"或"Digest"標志之后的信息;若是基本認證,此字符串是BASE64編碼的,則返回解碼后的字符串。對開(kāi)發(fā)人員來(lái)說(shuō),此方法不是必須的,但可以用于調試。
getAuth
功能:
取得用戶(hù)提交的認證信息。
原型:
public String getAuth(HttpServletRequest req)
參數:
req - HTTP請求
返回:
用戶(hù)提交的摘要認證信息中"Digest"或"Basic"標識之后信息;若是BASE64編碼,返回解碼后的字符串。
2. HttpBasicAuth類(lèi)
HttpBasicAuth類(lèi)實(shí)現HTTP基本認證。
HttpBasicAuth除了實(shí)現HttpAuth接口的所有方法外,還增加一個(gè)方法getUserNamePwd,用以取得客戶(hù)端提交的用戶(hù)名和密碼。在某些情況下,服務(wù)器端的數據庫或文件中并沒(méi)有保存用戶(hù)密碼的明文,而是保存對密碼經(jīng)過(guò)某種不可逆加密算法(MD5或其他)而得到的信息摘要(如UNIX系統)。如果是這樣,服務(wù)器端無(wú)法調用authenticate方法來(lái)進(jìn)行認證,只能先調用getUserNamePwd方法取得HTTP請求中的用戶(hù)名和密碼,然后按照數據庫或文件中的相同加密算法計算其摘要,最后比較所得到的摘要是否與數據庫或文件中的相同。這種情況下不能使用摘要認證,只能使用基本認證。
getUserNamePwd
功能:
取得用戶(hù)名密碼字符串數組。
原型:
public String[] getUserNamePwd(HttpServletRequest req)
參數:
req - HTTP請求
返回:
字符串數組,第一個(gè)元素為用戶(hù)名,第二個(gè)元素為密碼;若未取到用戶(hù)名和密碼,返回null。
HttpBasicAuth只有一個(gè)成員變量:
protected static BASE64Decoder base64Decoder = new BASE64Decoder();
即sun.misc.BASE64Decoder類(lèi)的一個(gè)實(shí)例,用于BASE64解碼。
3. HttpDigestAuth類(lèi)
HttpDigestAuth類(lèi)實(shí)現HTTP摘要認證。HttpDigestAuth類(lèi)除了實(shí)現HttpAuth接口外,根據摘要認證的特點(diǎn),還提供了其他一些方法。
客戶(hù)端提交的認證信息中,有一個(gè)nc值,表示臨時(shí)值nonce已被使用的次數;HttpDigestAuth類(lèi)的方法getNonceCount可取得此值。
getNonceCount
功能:
取得用戶(hù)發(fā)來(lái)的認證信息中的nc(NonceCount)值。
原型:
public int getNonceCount(HttpServletRequest req)
參數:
req - HTTP請求
返回:
返回nc值。若未取到,返回0。
對于這個(gè)nc值,服務(wù)器端根據自己的策略可以選擇不作限制,也可限定一個(gè)最大值。如果發(fā)現摘要正確但nonce使用次數超過(guò)上限,可以給客戶(hù)端返回一個(gè)“過(guò)期”質(zhì)詢(xún)響應,其WWW-Authorize消息頭中包含一個(gè)新的nonce值,并設置stale字段為true,以此要求客戶(hù)端使用新的nonce值重新計算摘要。此時(shí)客戶(hù)端不會(huì )重新彈出對話(huà)框讓用戶(hù)輸入密碼,而是用原來(lái)的密碼和新的nonce值重新計算摘要,然后重新發(fā)出請求。對客戶(hù)端用戶(hù)來(lái)說(shuō),這一過(guò)程是透明的。HttpDigestAuth提供一種參數形式的setUnauth方法,用以給客戶(hù)端返回“過(guò)期”質(zhì)詢(xún)。
setUnauth
功能:
設置 401 Unauthorized 狀態(tài)碼,并添加WWW-Authenticate消息頭。
原型:
public void setUnauth(HttpServletRequest req,
HttpServletResponse rsp,
String realm,
boolean stale)
參數:
req - HTTP請求
rsp - HTTP響應
realm - 指定realm字段的值,若為null則采用默認值servername:port
stale - 指定stale字段的值,true表示客戶(hù)端摘要正確,只是nonce值過(guò)期導致鑒權失??;false表示并非因為nonce值過(guò)期才鑒權失敗。
返回:
無(wú)
調用此方法,置參數stale為true,即向客戶(hù)端發(fā)揮“過(guò)期”質(zhì)詢(xún)。HttpDigestAuth中還有其他幾種形式的setUnau方法,凡是未指定stale的,均默認為false。
無(wú)論哪種參數形式的setUnauth方法,都必須每次生成一個(gè)新的nonce值。生成nonce的算法RFC2617并沒(méi)有做規定,HttpDigestAuth類(lèi)在generateNonce方法中生成nonce,算法為
NOnce = BCD( MD5( <client-IP>:<time-stamp>:<private-key> ) )
即:取得客戶(hù)端IP地址、當前時(shí)間(毫秒數),以及一個(gè)私有的key,將他們用冒號連接起來(lái),取其MD5摘要,然后將所得的字節數組轉換為十六進(jìn)制字符串(即BCD碼)。
五、應用舉例
這里通過(guò)一個(gè)例子來(lái)說(shuō)明如何使用Web平臺提供的API實(shí)現HTTP認證。
新建一個(gè)JSP文件AuthTest.jsp,源代碼如下(讀者可以將表格的右邊一列拷貝到一個(gè)文本文件中,另存為AuthTest.jsp即可)。將此文件部署到一個(gè)應用服務(wù)器中(如Tomcat等),啟動(dòng)服務(wù)器后,用IE請求這個(gè)JSP,即可看到瀏覽器彈出對話(huà)框提示輸入用戶(hù)名和密碼。
驗證時(shí),可以在IE中嘗試輸入正確或錯誤的用戶(hù)名、密碼,并可在對話(huà)框中選擇“取消”,看看效果如何。注意如果輸入了正確的用戶(hù)名和密碼,認證通過(guò)之后,IE不會(huì )再彈出對話(huà)框,除非重新啟動(dòng)一個(gè)IE窗口,或者修改JSP文件中的用戶(hù)名、密碼字符串導致認證失敗。
第11行生成HttpAuth接口的一個(gè)實(shí)例,即一個(gè)HttpBasicAuth對象,用以測試基本認證。第12行取得用戶(hù)名,然后判斷用戶(hù)名是否是“abc”,若不是,則在第15行設置未認證響應,指定realm值為“BasicAuthUser”,客戶(hù)端的對話(huà)框里會(huì )看到這個(gè)字符串。如果用戶(hù)名正確,第17行調用authenticate方法判斷用戶(hù)名/密碼是否為“abc”/“abcd”,若不是,在第19行再設置未認證響應,這次指定realm為“BasicAuthPwd”(這里兩次指定不同的realm值只是為了做驗證,實(shí)際應用中一般應該是一個(gè)相同的值)。若認證通過(guò),則第22行在頁(yè)面上輸出一個(gè)字符串表明已認證。最后不管是哪種情況,都在第51行給客戶(hù)端返回它自己提交的認證信息。
從第25行到第49行是測試HTTP摘要認證,這里被注釋掉了,驗證時(shí)可將其恢復,同時(shí)注釋掉第11到23行測試基本認證的部分。與基本認證相似,首先仍然是生成一個(gè)HttpAuth接口的實(shí)例,但這次是HttpDigestAuth對象;然后判斷用戶(hù)名和密碼是否正確。如果都正確,在第38行取出nc值,判斷其是否超過(guò)上限10,若超過(guò)則在第45行設置認證未通過(guò)(nonce過(guò)期)響應。
七、小結
本文介紹了HTTP認證的概念、規范以及ZX Web平臺提供的API。RFC2617中還涉及到使用代理服務(wù)器時(shí)HTTP認證的一些規范,這里就不作介紹了。
HTTP認證雖然不是安全的認證方式,但仍不失為一種簡(jiǎn)單易用的安全措施,在許多地方被采用。項目開(kāi)發(fā)中,在決定是否采用HTTP認證時(shí),一定要考慮方案的安全性,以及項目本身對安全性的要求;另一方面還要考慮瀏覽器對HTTP認證的支持。RFC2617發(fā)布時(shí),一般的瀏覽器都支持基本認證,而只有微軟的IE支持摘要認證。兩個(gè)y6
聯(lián)系客服