基本認證便捷靈活,但極不安全。用戶名和密碼都是以明文形式傳送的,也沒有采取任何措施防止對報文的篡改。安全使用基本認證的唯一方式就是將其與 SSL 配合使用。
摘要認證是另一種HTTP認證協議,它試圖修復Basci認證的嚴重缺陷,即進行如下改進:
1, 通過傳遞用戶名,密碼等計算出來的摘要來解決明文方式在網絡上發送密碼的問題。
2, 通過服務產生隨機數nonce的方式可以防止惡意用戶捕獲並重放認證的握手過程。
3, 通過客戶端產生隨機數cnonce的方式,支持客戶端對服務器的認證。
4, 通過對內容也加入摘要計算的方式,可以有選擇的防止對報文內容的篡改。
摘要認證並不是最安全的協議。摘要認證並不能滿足安全 HTTP 事務的很多需求。對這些需求來說,使用 TLS 和 HTTPS 協議更為合適一些。但摘要認證比它要取代的基本認證強大很多。
一,用摘要保護密碼
摘要認證的一個改進之處是用摘要代替密碼的傳輸,遵循的基本原則是“絕對不通過網絡發送明文密碼”,而是發送一個密碼的摘要信息,並且這摘要信息是不可逆的,即無法通 過摘要信息反推出密碼信息。而服務器本身是存儲這個密碼的(實際上,服務器只需知道密碼的摘要即可),而客戶端和服務器本身都知道這個密碼。這樣的話,服務器可以讀取客戶端的摘要和本身知道的密碼進行同樣計算得出的摘要進行比較,若匹配,則驗證通過。
摘要是對信息主體的濃縮,摘要是一種單向函數,主要用於將無限的輸入值轉為有限的濃縮輸出值,如MD5,則是將任意長度的字節系列轉換為一個128位的摘要。MD5輸出的128位的摘要通常會寫出32個十六進制的字符,每個字符表示4個bit。
二,用隨機數防止重放攻擊
使用單向摘要就無需以明文形式發送密碼了,可以只發送密碼的摘要,並且可以確信,沒有哪個惡意用戶能輕易的從摘要中解碼出原始密碼。
但是,摘要被截獲也可能跟密碼一起好用,為了防止重放攻擊的發送,服務器可以向客戶端發送一個稱為隨機數nonce的特殊令牌,這個數會經常發生變化(可能是每毫秒,或者每次認證都發生變化,具體由服務器控制),客戶端在計算摘要之前要先將這個隨機數附加到密碼上去。這樣,在密碼中加入隨機數就會使得摘要隨着隨機數的每次變化而變化,記錄下的密碼摘要只對特定的隨機數有效,而沒有密碼的話,攻擊者就無法計算出正確的摘要,這樣就可以防止重放攻擊的發生。
摘要認證要求使用隨機數,隨機數是在WWW-Authenticate服務器質詢響應中從服務器傳輸給客戶端的。
三,摘要認證的握手過程
1, 第一次客戶端請求的時候,服務器產生一個隨機數nonce,服務器將這個隨機數放在WWW-Authenticate響應頭,與服務器支持的認證算法列表,認證的域realm一起發送給客戶端,如下例子:
HTTP /1.1 401 Unauthorized
WWW-Authenticate:Digest
realm= ”test realm”
qop=auth,auth-int”
nonce=”66C4EF58DA7CB956BD04233FBB64E0A4”
2, 客戶端發現是401響應,表示需要進行認證,則彈出讓用戶輸入用戶名和密碼的認證窗口,客戶端選擇一個算法,計算出密碼和其他數據的摘要,將摘要放到Authorization的請求頭中發送給服務器,如果客戶端要對服務器也進行認證,這個時候,可以發送客戶端隨機數cnonce。如下例子:
GET/cgi-bin/checkout?a=b HTTP/1.1
Authorization: Digest
username=”tenfyguo”
realm=”test realm”
nonce=” 66C4EF58DA7CB956BD04233FBB64E0A4” //服務器端的隨機數一起帶回
uri=”/cgi-bin/checkout?a=b” //必須跟請求行一致
qop=”auth” //保護質量參數
nc=0000001
cnonce=”xxxxx234132543strwerr65sgdrftdfytryts” //客戶端隨機數,用於對稱校驗
response=” ABC4EF58DA7CB956BD04345FBB64E0A4”//最終摘要
3, 服務接受摘要,選擇算法以及掌握的數據,重新計算新的摘要跟客戶端傳輸的摘要進行比較,驗證是否匹配,若客戶端反過來用客戶端隨機數對服務器進行質詢,就會創建客戶端摘要,服務可以預先將下一個隨機數計算出來,提前傳遞給客戶端,通過Authentication-Info發送下一個隨機數。如下例子:
HTTP/1.1 200 OK
Authorization-Info:nextnonce=” 88C4EF58DA7CB956BD04233FBB64E0A4”
qop=”auth”
rspauth=”23543534DfasetwerwgDTerGDTERERRE”
cnonce=” xxxxx234132543strwerr65sgdrftdfytryts”
四,摘要的計算
在說明如何計算摘要之前,先說明參加摘要計算的信息塊。信息塊主要有兩種:
1,表示與安全相關的數據的A1。
A1中的數據時密碼和受保護信息的產物,它包括用戶名,密碼,保護域和隨機數等內容,A1只涉及安全信息,與底層報文自身無關。
若算法是:MD5
則A1=<user>:<realm>:<password>
若算法是:MD5-sess
則A1=MD5(<user>:<realm>:<password>):<nonce>:<cnonce>
2,表示與報文相關的數據的A2.
A2表示是與報文自身相關的信息,比如URL,請求反復和報文實體的主體部分,A2加入摘要計算主要目的是有助於防止反復,資源或者報文被篡改。
若qop未定義或者auth:
A2=<request-method>:<uri-directive-value>
若qop為auth:-int
A2=<request-method>:<uri-directive-value>:MD5(<request-entity-body>)
下面定義摘要的計算規則:
若qop沒有定義:
摘要response=MD5(MD5(A1):<nonce>:MD5(A2))
若qop為auth:
摘要response=MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))
若qop為auth-int:
摘要response= MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2))
五,隨機數的生成
RFC2617建議采用這個假想的隨機數公式:
nonce = BASE64(time-stamp MD5(time-stamp “:” ETag “:” private-key))
其中:
time-stamp是服務器產生的時間戳或者其他不會重復的序列號,ETag是與所請求實體有關的HTTP ETag首部的值,priviate-key是只有服務器知道的數據。
這樣,服務器就可以收到客戶端的認證首部之后重新計算散列部分,如果結果與那個首部的隨機數不符,或者是時間戳的值不夠新,就可以拒絕請求,服務器可以通過這種方式來限制隨機數的有效持續時間。
包括了ETag可以防止對已經更新資源版本的重放請求。注意:在隨機數中包含客戶端IP,服務器好像就可以限制原來獲取此隨機數的客戶端重用這個隨機數了,但這會破壞代理集群的工作,使用代理集群時候,來自單個用戶的多條請求通常會經過不同的代理進行傳輸,而且IP地址欺騙實現起來也不復雜。
六,摘要認證的工作原理
下面來看看摘要認證的工作原理(簡化版本):
a) 客戶端請求了某個受保護文檔。
b) 在客戶端能夠證明其知道密碼從而確認其身份之前,服務器拒絕提供文檔。服務器向客戶端發起質詢,詢問用戶名和摘要形式的密碼。
c) 客戶端傳遞了密碼的摘要,證明它是知道密碼的。服務器知道所有用戶的秘密,因此可以將客戶提供的摘要與服務器自己計算得到的摘要進行比較,以驗證用戶是否知道密碼。另一方在不知道密碼的情況下,很難偽造出正確的摘要。
d) 服務器將客戶端提供的摘要與服務器內部計算出的摘要進行對比。如果匹配,就說明客戶端知道密碼(或者很幸運地猜中了!)。可以設置摘要函數,使其產生很多數字,讓人不可能幸運地猜中摘要。服務器進行了匹配驗證之后,會將文檔提供給客戶端——整個過程都沒有在網絡上發送密碼。
七,重放攻擊
使用摘要就無需以明文形式發送密碼了。可以只發送密碼的摘要,而且可以確信,沒有哪個惡意用戶能輕易地從摘要中解碼出原始密碼。
但是,僅僅隱藏密碼並不能避免危險,因為即便不知道密碼,別有用心的人也可以截獲摘要,並一遍遍地重放給服務器。摘要和密碼一樣好用。
為防止此類重放攻擊的發生,服務器可以向客戶端發送了一個稱為隨機數 (nonce) 的特殊令牌,這個數會經常發生變化(可能是每毫秒,或者是每次認證都變化)。客戶端在計算摘要之前要先將這個隨機數令牌附加到密碼上去。
在密碼中加入隨機數就會使摘要隨着隨機數的每一次變化而變化。記錄下的密碼摘要只對特定的隨機數有效,而沒有密碼的話,攻擊者就無法計算出正確的摘要,這樣就可以防止重放攻擊的發生。
摘要認證要求使用隨機數,因為這個小小的重放弱點會使未隨機化的摘要認證變得和基本認證一樣脆弱。隨機數是在 WWW-Authenticate 質詢中從服務器傳送給客戶端。
八,摘要認證的握手機制
下面是簡化的摘要認證三步握手機制:
(1) 服務器計算出一個隨機數。
(2) 服務器將這個隨機數放在 WWW-Authenticate 質詢報文中,與服務器所支持的算法列表一同發往客戶端。
(3) 客戶端選擇一個算法,計算出密碼和其他數據的摘要。
(4) 客戶端將摘要放在一條 Authorization 報文中發回服務器。如果客戶端要對服務器進行認證,可以發送客戶端隨機數。
(5) 服務器接收摘要、選中的算法以及支撐數據,計算出與客戶端相同的摘要。然后服務器將本地生成的摘要與網絡傳送過來的摘要進行比較,驗證是否匹配。如果客戶端反過來用客戶端隨機數對服務器進行質詢,就會創建客戶端摘要。服務器可以預先將下一個隨機數計算出來,提前將其傳遞給客戶端,這樣下一次客戶端就可以預先發送正確的摘要了。
九,預授權
普通的認證方式中,事務結束之前,每條請求都要有一次請求/質詢的需要,參見下圖 (a)。
如果客戶端事先知道下一個隨機數是什么,就可以取消這個請求/質詢循環,這樣客戶端就可以在服務器發出請求之前,生成正確的 Authorization 首部了。如果客戶端能在服務器要求他計算 Authorization 首部之前將其計算出來,就可以預先將 Authorization 首部發送給服務器,而不用進行請求/質詢了。下圖 (b) 顯示了這種方式對性能的影響。
預授權對基本認證來說並不重要(而且很常見)。瀏覽器通常會維護一些客戶端數據庫以存儲用戶名和密碼。一旦用戶與某站點進行了認證,瀏覽器通常會為后繼對那個 URL 的請求發送正確的 Authorization 首部。
由於摘要認證使用了隨機數技術來破壞重放攻擊,所以對摘要認證來說,預授權要稍微復雜一些。服務器會產生任意的隨機數,所以在客戶端收到質詢之前,不一定總能判定應該發送什么樣的 Authorization 首部。
摘要認證在保留了很多安全特性的同時,還提供了集中預授權方式。這里列出了三種可選的方式,通過這些方式,客戶端無效等待新的 WWW-Authenticate 質詢,就可以獲得正確的隨機數:
- 服務器預先在 Authentication-Info 成功首部中發送下一個隨機數;
- 服務器允許在一小段時間內使用同一個隨機數;
- 客戶端和服務器使用同步的、可預測的隨機數算法。
預先生成下一個隨機數
可以在 Authentication-Info 成功首部中將下一個隨機數預先提供給客戶端。這個首部是與前一次成功認證的 200 OK 響應一同發送的。
Authentication-Info: nextnonce="<nonce-value>"
有了下一個隨機數,客戶端就可以預先發布 Authorization 首部了。
盡管這種預授權機制避免了請求/質詢循環(加快了事務處理的速度),但實際上它也破壞了對同一台服務器的多條請求進行管道化的功能,因為在發布下一條請求之前,一定要收到下一個隨機值才行。而管道化是避免延遲的一項基本技術。所以這樣可能會造成很大的性能損失。
受限制的隨機數重用機制
另一種方法不是預先生成隨機數序列,而是在有限的次數內重用隨機數。比如,服務器可能允許將某個隨機重用 5 次,或者重用 10 秒。
在這種情況下,客戶端可以隨意發布帶有 Authorization 首部的請求,而且由於隨機數是事先知道的,所以還可以對請求進行管道化。隨機數過期時,服務器要向客戶端發送 401 Unauthorized 質詢,並設置 WWW-Authenticate:stale=true 指令:
WWW-Authenticate: Digest realm="<realm-value>", nonce="<nonce-value>", stale=true
重用隨機數使得攻擊者更容易成功地實行重放攻擊。雖然這確實降低了安全性,但重用的隨機數的生存周期是可控的(從嚴格禁止重用到較長時間的重用),所以應該可以在安全和性能間找到平衡。
同步生成隨機數
還可以采用時間同步的隨機數生成算法,客戶端和服務器可根據共享的密鑰,生成第三方無法輕易預測的、相同的隨機數序列。
文章最后附上HttpClient4.2.3以及HttpClient4.3摘要認證親測實例:
public static void httpSend(String url,String userName,String passWord,String xmlParam){ CloseableHttpClient httpClient = null; try { URI serverURI = new URI(url); //摘要認證處理 /*已過時的:DefaultHttpClient httpClient = new DefaultHttpClient(); Credentials creds = new UsernamePasswordCredentials(userName,passWord); httpClient.getCredentialsProvider().setCredentials(new AuthScope(serverURI.getHost(), serverURI.getPort()), (Credentials) creds); httpClient.getParams().setParameter(AuthSchemes.DIGEST, Collections.singleton(AuthSchemes.DIGEST)); httpClient.getAuthSchemes().register(AuthSchemes.DIGEST,new DigestSchemeFactory());*/ //設置超時代碼 /*RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(5000).setConnectionRequestTimeout(3000) .setSocketTimeout(3000).build(); httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build();*/ CredentialsProvider credsProvider = new BasicCredentialsProvider(); credsProvider.setCredentials(new AuthScope(serverURI.getHost(), serverURI.getPort()), new UsernamePasswordCredentials(userName,passWord)); httpClient = HttpClients.custom().setDefaultCredentialsProvider(credsProvider).build(); HttpPost post = new HttpPost(url); // 構造消息頭 /*post.setHeader("Content-type", "application/json; charset=utf-8");*/ // post.setEntity(new ByteArrayEntity(bytes));//發送二進制數組數據如:圖片的二進制數據 post.setEntity(new StringEntity(xmlParam,"UTF-8"));//發送xml字符串數據 HttpResponse response = httpClient.execute(post); String result = EntityUtils.toString(response.getEntity()); System.out.println("返回的消息:" + result); System.out.println("返回的狀態" + response.getStatusLine().getStatusCode()); System.out.println(response.getEntity().getContentType()); EntityUtils.consume(response.getEntity()); httpClient.close(); } catch (Exception e) { e.printStackTrace(); }finally{ if(httpClient != null){ try { httpClient.close(); } catch (IOException e) { e.printStackTrace(); } } } } private static String getXmlString() { StringBuilder sb=new StringBuilder(); try { InputStream inputStream = DigestDemo.class.getResourceAsStream("searchFc2.xml"); BufferedReader br=new BufferedReader(new InputStreamReader(inputStream)); String line=""; for(line=br.readLine();line!=null;line=br.readLine()) { sb.append(line+"\n"); } } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return sb.toString(); } public static void main(String[] args) throws Exception { String url = "http://"; String userName = ""; String passWord = ""; String xmlParam = getXmlString(); httpSend(url,userName,passWord,xmlParam); } //測試接收方部分代碼 public void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ... request.setCharacterEncoding("utf-8"); BufferedReader reader = null; String result = ""; StringBuffer sbf = new StringBuffer(); InputStream is = request.getInputStream(); reader = new BufferedReader(new InputStreamReader(is, "utf-8")); String strRead = null; while ((strRead = reader.readLine()) != null) { sbf.append(strRead); sbf.append("\r\n"); } reader.close(); result = sbf.toString(); System.out.println("result:"+result); ... }