1. 程式人生 > >WebApi介面安全認證——HTTP之摘要認證

WebApi介面安全認證——HTTP之摘要認證

摘要訪問認證是一種協議規定的Web伺服器用來同網頁瀏覽器進行認證資訊協商的方法。它在密碼發出前,先對其應用雜湊函式,這相對於HTTP基本認證傳送明文而言,更安全。從技術上講,摘要認證是使用隨機數來阻止進行密碼分析的MD5加密雜湊函式應用。它使用HTTP協議。

一、摘要認證基本流程:
1.客戶端請求 (無認證)`

GET /dir/index.html HTTP/1.0
Host: localhost

2.伺服器響應

服務端返回401未驗證的狀態,並且返回WWW-Authenticate資訊,包含了驗證方式Digest,realm,qop,nonce,opaque的值。其中:

Digest:認證方式;

realm:領域,領域引數是強制的,在所有的盤問中都必須有,它的目的是鑑別SIP訊息中的機密,在SIP實際應用中,它通常設定為SIP代理伺服器所負責的域名;

qop:保護的質量,這個引數規定伺服器支援哪種保護方案,客戶端可以從列表中選擇一個。值 “auth”表示只進行身份查驗, “auth-int”表示進行查驗外,還有一些完整性保護。需要看更詳細的描述,請參閱RFC2617;

nonce:為一串隨機值,在下面的請求中會一直使用到,當過了存活期後服務端將重新整理生成一個新的nonce值;

opaque:一個不透明的(不讓外人知道其意義)資料字串,在盤問中傳送給使用者。

HTTP/1.0 401 Unauthorized
Server: HTTPd/0.9
Date: Sun, 10 Apr 2005 20:26:47 GMT
WWW-Authenticate: Digest realm="[email protected]",
                        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="[email protected]",
                     nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
                     uri="/dir/index.html",
                     qop=auth,
                     nc=00000001,
                     cnonce="0a4f113b",
                     response="6629fae49393a05397450978507c4ef1",
                     opaque="5ccc069c403ebaf9f0171e9517f40e41"

4.伺服器響應

當伺服器接收到摘要響應,也要重新計算響應中各引數的值,並利用客戶端提供的引數值,和伺服器上儲存的口令,進行比對。如果計算結果與收到的客戶響應值是相同的,則客戶已證明它知道口令,因而客戶的身份驗證通過。

HTTP/1.0 200 OK

二、服務端驗證

編寫一個自定義訊息處理器,需要通過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();  
    }  
}  

nonce類:

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;  
    }  
}  

需要使用摘要驗證可在程式碼裡新增Attribute [Authorize],如:

[Authorize]  
public class ProductsController : ApiController  

最後Global.asax裡需註冊下:

GlobalConfiguration.Configuration.MessageHandlers.Add(  
new AuthenticationHandler());  

三、客戶端的呼叫
這裡主要說明使用WebClient呼叫

public static string Request(string sUrl, string sMethod, string sEntity, string sContentType,  
    out string sMessage)  
{  
    try  
    {  
        sMessage = "";  
        using (System.Net.WebClient client = new System.Net.WebClient())  
        {  
            client.Credentials = CreateAuthenticateValue(sUrl);  
            client.Headers = CreateHeader(sContentType);  
  
            Uri url = new Uri(sUrl);  
            byte[] bytes = Encoding.UTF8.GetBytes(sEntity);  
            byte[] buffer;  
            switch (sMethod.ToUpper())  
            {  
                case "GET":  
                    buffer = client.DownloadData(url);  
                    break;  
                case "POST":  
                    buffer = client.UploadData(url, "POST", bytes);  
                    break;  
                default:  
                    buffer = client.UploadData(url, "POST", bytes);  
                    break;  
            }  
  
            return Encoding.UTF8.GetString(buffer);  
        }  
    }  
    catch (WebException ex)  
    {  
        sMessage = ex.Message;  
        var rsp = ex.Response as HttpWebResponse;  
        var httpStatusCode = rsp.StatusCode;  
        var authenticate = rsp.Headers.Get("WWW-Authenticate");  
  
        return "";  
    }  
    catch (Exception ex)  
    {  
        sMessage = ex.Message;  
        return "";  
    }  
}  

關鍵程式碼,在這裡新增使用者認證,使用NetworkCredential:

private static CredentialCache CreateAuthenticateValue(string sUrl)  
{  
    CredentialCache credentialCache = new CredentialCache();  
    credentialCache.Add(new Uri(sUrl), "Digest", new NetworkCredential("Lime", "Lime"));  
  
    return credentialCache;  
}