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