摘要訪問認證是一種協議規定的Web服務器用來同網頁瀏覽器進行認證信息協商的方法。它在密碼發出前,先對其應用哈希函數,這相對於HTTP基本認證發送明文而言,更安全。從技術上講,摘要認證是使用隨機數來阻止進行密碼分析的MD5加密哈希函數應用。它使用HTTP協議。html
1、摘要認證基本流程:瀏覽器
1.客戶端請求 (無認證)安全
- GET /dir/index.html HTTP/1.0
- Host: localhost
2.服務器響應服務器
服務端返回401未驗證的狀態,而且返回WWW-Authenticate信息,包含了驗證方式Digest,realm,qop,nonce,opaque的值。其中:async
Digest:認證方式;ide
realm:領域,領域參數是強制的,在全部的盤問中都必須有,它的目的是鑑別SIP消息中的機密,在SIP實際應用中,它一般設置爲SIP代理服務器所負責的域名;函數
qop:保護的質量,這個參數規定服務器支持哪一種保護方案,客戶端能夠從列表中選擇一個。值 「auth」表示只進行身份查驗, 「auth-int」表示進行查驗外,還有一些完整性保護。須要看更詳細的描述,請參閱RFC2617;ui
nonce:爲一串隨機值,在下面的請求中會一直使用到,當過了存活期後服務端將刷新生成一個新的nonce值;this
opaque:一個不透明的(不讓外人知道其意義)數據字符串,在盤問中發送給用戶。加密
- HTTP/1.0 401 Unauthorized
- Server: HTTPd/0.9
- Date: Sun, 10 Apr 2005 20:26:47 GMT
- WWW-Authenticate: Digest realm="testrealm@host.com",
- qop="auth,auth-int",
- nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
- opaque="5ccc069c403ebaf9f0171e9517f40e41"
3.客戶端請求 (用戶名 "Mufasa", 密碼 "Circle Of Life")
客戶端接受到請求返回後,進行HASH運算,返回Authorization參數
其中:realm,nonce,qop由服務器產生;
uri:客戶端想要訪問的URI;
nc:「現時」計數器,這是一個16進制的數值,即客戶端發送出請求的數量(包括當前這個請求),這些請求都使用了當前請求中這個「現時」值。例如,對一個給定的「現時」值,在響應的第一個請求中,客戶端將發送「nc=00000001」。這個指示值的目的,是讓服務器保持這個計數器的一個副本,以便檢測重複的請求。若是這個相同的值看到了兩次,則這個請求是重複的;
cnonce:這是一個不透明的字符串值,由客戶端提供,而且客戶端和服務器都會使用,以免用明文文本。這使得雙方均可以查驗對方的身份,並對消息的完整性提供一些保護;
response:這是由用戶代理軟件計算出的一個字符串,以證實用戶知道口令。
- <strong>response計算過程:</strong>
- HA1=MD5(A1)=MD5(username:realm:password)
- 若是 qop 值爲「auth」或未指定,那麼 HA2 爲
- HA2=MD5(A2)=MD5(method:digestURI)
- 若是 qop 值爲「auth-int」,那麼 HA2 爲
- HA2=MD5(A2)=MD5(method:digestURI:MD5(entityBody))
- 若是 qop 值爲「auth」或「auth-int」,那麼以下計算 response:
- response=MD5(HA1:nonce:nonceCount:clientNonce:qop:HA2)
- 若是 qop 未指定,那麼以下計算 response:
- response=MD5(HA1:nonce:HA2)
請求頭:
- GET /dir/index.html HTTP/1.0
- Host: localhost
- Authorization: Digest username="Mufasa",
- realm="testrealm@host.com",
- nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
- uri="/dir/index.html",
- qop=auth,
- nc=00000001,
- cnonce="0a4f113b",
- response="6629fae49393a05397450978507c4ef1",
- opaque="5ccc069c403ebaf9f0171e9517f40e41"
4.服務器響應
當服務器接收到摘要響應,也要從新計算響應中各參數的值,並利用客戶端提供的參數值,和服務器上存儲的口令,進行比對。若是計算結果與收到的客戶響應值是相同的,則客戶已證實它知道口令,於是客戶的身份驗證經過。
- HTTP/1.0 200 OK
2、服務端驗證
編寫一個自定義消息處理器,須要經過System.Net.Http.DelegatingHandler進行派生,並重寫SendAsync方法。
- public class AuthenticationHandler : DelegatingHandler
- {
- protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
- {
- try
- {
- HttpRequestHeaders headers = request.Headers;
- if (headers.Authorization != null)
- {
- Header header = new Header(request.Headers.Authorization.Parameter, request.Method.Method);
- if (Nonce.IsValid(header.Nonce, header.NounceCounter))
- {
- // Just assuming password is same as username for the purpose of illustration
- string password = header.UserName;
- string ha1 = String.Format("{0}:{1}:{2}", header.UserName, header.Realm, password).ToMD5Hash();
- string ha2 = String.Format("{0}:{1}", header.Method, header.Uri).ToMD5Hash();
- string computedResponse = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",
- ha1, header.Nonce, header.NounceCounter,header.Cnonce, "auth", ha2).ToMD5Hash();
- if (String.CompareOrdinal(header.Response, computedResponse) == 0)
- {
- // digest computed matches the value sent by client in the response field.
- // Looks like an authentic client! Create a principal.
- var claims = new List<Claim>
- {
- new Claim(ClaimTypes.Name, header.UserName),
- new Claim(ClaimTypes.AuthenticationMethod, AuthenticationMethods.Password)
- };
- ClaimsPrincipal principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") });
- Thread.CurrentPrincipal = principal;
- if (HttpContext.Current != null)
- HttpContext.Current.User = principal;
- }
- }
- }
- HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
- if (response.StatusCode == HttpStatusCode.Unauthorized)
- {
- response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest",Header.UnauthorizedResponseHeader.ToString()));
- }
- return response;
- }
- catch (Exception)
- {
- var response = request.CreateResponse(HttpStatusCode.Unauthorized);
- response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest",Header.UnauthorizedResponseHeader.ToString()));
- return response;
- }
- }
- }
Header類
- public class Header
- {
- public Header() { }
- public Header(string header, string method)
- {
- string keyValuePairs = header.Replace("\"", String.Empty);
- foreach (string keyValuePair in keyValuePairs.Split(','))
- {
- int index = keyValuePair.IndexOf("=", System.StringComparison.Ordinal);
- string key = keyValuePair.Substring(0, index);
- string value = keyValuePair.Substring(index + 1);
- switch (key)
- {
- case "username": this.UserName = value; break;
- case "realm": this.Realm = value; break;
- case "nonce": this.Nonce = value; break;
- case "uri": this.Uri = value; break;
- case "nc": this.NounceCounter = value; break;
- case "cnonce": this.Cnonce = value; break;
- case "response": this.Response = value; break;
- case "method": this.Method = value; break;
- }
- }
- if (String.IsNullOrEmpty(this.Method))
- this.Method = method;
- }
- public string Cnonce { get; private set; }
- public string Nonce { get; private set; }
- public string Realm { get; private set; }
- public string UserName { get; private set; }
- public string Uri { get; private set; }
- public string Response { get; private set; }
- public string Method { get; private set; }
- public string NounceCounter { get; private set; }
- // This property is used by the handler to generate a
- // nonce and get it ready to be packaged in the
- // WWW-Authenticate header, as part of 401 response
- public static Header UnauthorizedResponseHeader
- {
- get
- {
- return new Header()
- {
- Realm = "MyRealm",
- Nonce = WebApiDemo.Nonce.Generate()
- };
- }
- }
- public override string ToString()
- {
- StringBuilder header = new StringBuilder();
- header.AppendFormat("realm=\"{0}\"", Realm);
- header.AppendFormat(",nonce=\"{0}\"", Nonce);
- header.AppendFormat(",qop=\"{0}\"", "auth");
- return header.ToString();
- }
- }
- public class Nonce
- {
- private static ConcurrentDictionary<string, Tuple<int, DateTime>>
- nonces = new ConcurrentDictionary<string, Tuple<int, DateTime>>();
- public static string Generate()
- {
- byte[] bytes = new byte[16];
- using (var rngProvider = new RNGCryptoServiceProvider())
- {
- rngProvider.GetBytes(bytes);
- }
- string nonce = bytes.ToMD5Hash();
- nonces.TryAdd(nonce, new Tuple<int, DateTime>(0, DateTime.Now.AddMinutes(10)));
- return nonce;
- }
- public static bool IsValid(string nonce, string nonceCount)
- {
- Tuple<int, DateTime> cachedNonce = null;
- //nonces.TryGetValue(nonce, out cachedNonce);
- nonces.TryRemove(nonce, out cachedNonce);//每一個nonce只容許使用一次
- if (cachedNonce != null) // nonce is found
- {
- // nonce count is greater than the one in record
- if (Int32.Parse(nonceCount) > cachedNonce.Item1)
- {
- // nonce has not expired yet
- if (cachedNonce.Item2 > DateTime.Now)
- {
- // update the dictionary to reflect the nonce count just received in this request
- //nonces[nonce] = new Tuple<int, DateTime>(Int32.Parse(nonceCount), cachedNonce.Item2);
- // Every thing looks ok - server nonce is fresh and nonce count seems to be
- // incremented. Does not look like replay.
- return true;
- }
- }
- }
- return false;
- }
- }