• WebApi接口安全认证——HTTP之摘要认证


         摘要访问认证是一种协议规定的Web服务器用来同网页浏览器进行认证信息协商的方法。它在密码发出前,先对其应用哈希函数,这相对于HTTP基本认证发送明文而言,更安全。从技术上讲,摘要认证是使用随机数来阻止进行密码分析的MD5加密哈希函数应用。它使用HTTP协议。

    一、摘要认证基本流程:

    1.客户端请求 (无认证)

    Html代码 复制代码 收藏代码
    1. GET /dir/index.html HTTP/1.0  
    2. Host: localhost  
    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:一个不透明的(不让外人知道其意义)数据字符串,在盘问中发送给用户。

    Html代码 复制代码 收藏代码
    1. HTTP/1.0 401 Unauthorized  
    2. Server: HTTPd/0.9  
    3. Date: Sun, 10 Apr 2005 20:26:47 GMT  
    4. WWW-Authenticate: Digest realm="testrealm@host.com",  
    5.                         qop="auth,auth-int",  
    6.                         nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",  
    7.                         opaque="5ccc069c403ebaf9f0171e9517f40e41"  
    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:这是由用户代理软件计算出的一个字符串,以证明用户知道口令。

    Html代码 复制代码 收藏代码
    1. <strong>response计算过程:</strong>  
    2. HA1=MD5(A1)=MD5(username:realm:password)  
    3. 如果 qop 值为“auth”或未指定,那么 HA2 为  
    4. HA2=MD5(A2)=MD5(method:digestURI)  
    5. 如果 qop 值为“auth-int”,那么 HA2 为  
    6. HA2=MD5(A2)=MD5(method:digestURI:MD5(entityBody))  
    7. 如果 qop 值为“auth”或“auth-int”,那么如下计算 response:  
    8. response=MD5(HA1:nonce:nonceCount:clientNonce:qop:HA2)  
    9. 如果 qop 未指定,那么如下计算 response:  
    10. response=MD5(HA1:nonce:HA2)  
    response计算过程:
    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)
    

     请求头:

    Html代码 复制代码 收藏代码
    1. GET /dir/index.html HTTP/1.0  
    2. Host: localhost  
    3. Authorization: Digest username="Mufasa",  
    4.                      realm="testrealm@host.com",  
    5.                      nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",  
    6.                      uri="/dir/index.html",  
    7.                      qop=auth,  
    8.                      nc=00000001,  
    9.                      cnonce="0a4f113b",  
    10.                      response="6629fae49393a05397450978507c4ef1",  
    11.                      opaque="5ccc069c403ebaf9f0171e9517f40e41"  
    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.服务器响应

    当服务器接收到摘要响应,也要重新计算响应中各参数的值,并利用客户端提供的参数值,和服务器上存储的口令,进行比对。如果计算结果与收到的客户响应值是相同的,则客户已证明它知道口令,因而客户的身份验证通过。

    Html代码 复制代码 收藏代码
    1. HTTP/1.0 200 OK  
    HTTP/1.0 200 OK
    

    二、服务端验证

    编写一个自定义消息处理器,需要通过System.Net.Http.DelegatingHandler进行派生,并重写SendAsync方法。

    C#代码 复制代码 收藏代码
    1. public class AuthenticationHandler : DelegatingHandler  
    2. {  
    3.     protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
    4.     {  
    5.         try  
    6.         {  
    7.             HttpRequestHeaders headers = request.Headers;  
    8.             if (headers.Authorization != null)  
    9.             {  
    10.                 Header header = new Header(request.Headers.Authorization.Parameter, request.Method.Method);  
    11.   
    12.                 if (Nonce.IsValid(header.Nonce, header.NounceCounter))  
    13.                 {  
    14.                     // Just assuming password is same as username for the purpose of illustration  
    15.                     string password = header.UserName;  
    16.   
    17.                     string ha1 = String.Format("{0}:{1}:{2}", header.UserName, header.Realm, password).ToMD5Hash();  
    18.   
    19.                     string ha2 = String.Format("{0}:{1}", header.Method, header.Uri).ToMD5Hash();  
    20.   
    21.                     string computedResponse = String.Format("{0}:{1}:{2}:{3}:{4}:{5}",  
    22.                                         ha1, header.Nonce, header.NounceCounter,header.Cnonce, "auth", ha2).ToMD5Hash();  
    23.   
    24.                     if (String.CompareOrdinal(header.Response, computedResponse) == 0)  
    25.                     {  
    26.                         // digest computed matches the value sent by client in the response field.  
    27.                         // Looks like an authentic client! Create a principal.  
    28.                         var claims = new List<Claim>  
    29.                         {  
    30.                                         new Claim(ClaimTypes.Name, header.UserName),  
    31.                                         new Claim(ClaimTypes.AuthenticationMethod, AuthenticationMethods.Password)  
    32.                         };  
    33.   
    34.                         ClaimsPrincipal principal = new ClaimsPrincipal(new[] { new ClaimsIdentity(claims, "Digest") });  
    35.   
    36.                         Thread.CurrentPrincipal = principal;  
    37.   
    38.                         if (HttpContext.Current != null)  
    39.                             HttpContext.Current.User = principal;  
    40.                     }  
    41.                 }  
    42.             }  
    43.   
    44.             HttpResponseMessage response = await base.SendAsync(request, cancellationToken);  
    45.   
    46.             if (response.StatusCode == HttpStatusCode.Unauthorized)  
    47.             {  
    48.                 response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest",Header.UnauthorizedResponseHeader.ToString()));  
    49.             }  
    50.   
    51.             return response;  
    52.         }  
    53.         catch (Exception)  
    54.         {  
    55.             var response = request.CreateResponse(HttpStatusCode.Unauthorized);  
    56.             response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Digest",Header.UnauthorizedResponseHeader.ToString()));  
    57.   
    58.             return response;  
    59.         }  
    60.     }  
    61. }  
    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类

    C#代码 复制代码 收藏代码
    1. public class Header  
    2. {  
    3.     public Header() { }  
    4.   
    5.     public Header(string header, string method)  
    6.     {  
    7.         string keyValuePairs = header.Replace(""", String.Empty);  
    8.   
    9.         foreach (string keyValuePair in keyValuePairs.Split(','))  
    10.         {  
    11.             int index = keyValuePair.IndexOf("=", System.StringComparison.Ordinal);  
    12.             string key = keyValuePair.Substring(0, index);  
    13.             string value = keyValuePair.Substring(index + 1);  
    14.   
    15.             switch (key)  
    16.             {  
    17.                 case "username": this.UserName = value; break;  
    18.                 case "realm": this.Realm = value; break;  
    19.                 case "nonce": this.Nonce = value; break;  
    20.                 case "uri": this.Uri = value; break;  
    21.                 case "nc": this.NounceCounter = value; break;  
    22.                 case "cnonce": this.Cnonce = value; break;  
    23.                 case "response": this.Response = value; break;  
    24.                 case "method": this.Method = value; break;  
    25.             }  
    26.         }  
    27.   
    28.         if (String.IsNullOrEmpty(this.Method))  
    29.             this.Method = method;  
    30.     }  
    31.   
    32.     public string Cnonce { get; private set; }  
    33.     public string Nonce { get; private set; }  
    34.     public string Realm { get; private set; }  
    35.     public string UserName { get; private set; }  
    36.     public string Uri { get; private set; }  
    37.     public string Response { get; private set; }  
    38.     public string Method { get; private set; }  
    39.     public string NounceCounter { get; private set; }  
    40.   
    41.     // This property is used by the handler to generate a  
    42.     // nonce and get it ready to be packaged in the  
    43.     // WWW-Authenticate header, as part of 401 response  
    44.     public static Header UnauthorizedResponseHeader  
    45.     {  
    46.         get  
    47.         {  
    48.             return new Header()  
    49.             {  
    50.                 Realm = "MyRealm",  
    51.                 Nonce = WebApiDemo.Nonce.Generate()  
    52.             };  
    53.         }  
    54.     }  
    55.   
    56.     public override string ToString()  
    57.     {  
    58.         StringBuilder header = new StringBuilder();  
    59.         header.AppendFormat("realm="{0}"", Realm);  
    60.         header.AppendFormat(",nonce="{0}"", Nonce);  
    61.         header.AppendFormat(",qop="{0}"", "auth");  
    62.         return header.ToString();  
    63.     }  
    64. }  
    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类

    C#代码 复制代码 收藏代码
    1. public class Nonce  
    2. {  
    3.     private static ConcurrentDictionary<string, Tuple<int, DateTime>>  
    4.     nonces = new ConcurrentDictionary<string, Tuple<int, DateTime>>();  
    5.   
    6.     public static string Generate()  
    7.     {  
    8.         byte[] bytes = new byte[16];  
    9.   
    10.         using (var rngProvider = new RNGCryptoServiceProvider())  
    11.         {  
    12.             rngProvider.GetBytes(bytes);  
    13.         }  
    14.   
    15.         string nonce = bytes.ToMD5Hash();  
    16.   
    17.         nonces.TryAdd(nonce, new Tuple<int, DateTime>(0, DateTime.Now.AddMinutes(10)));  
    18.   
    19.         return nonce;  
    20.     }  
    21.   
    22.     public static bool IsValid(string nonce, string nonceCount)  
    23.     {  
    24.         Tuple<int, DateTime> cachedNonce = null;  
    25.         //nonces.TryGetValue(nonce, out cachedNonce);  
    26.         nonces.TryRemove(nonce, out cachedNonce);//每个nonce只允许使用一次  
    27.   
    28.         if (cachedNonce != null) // nonce is found  
    29.         {  
    30.             // nonce count is greater than the one in record  
    31.             if (Int32.Parse(nonceCount) > cachedNonce.Item1)  
    32.             {  
    33.                 // nonce has not expired yet  
    34.                 if (cachedNonce.Item2 > DateTime.Now)  
    35.                 {  
    36.                     // update the dictionary to reflect the nonce count just received in this request  
    37.                     //nonces[nonce] = new Tuple<int, DateTime>(Int32.Parse(nonceCount), cachedNonce.Item2);  
    38.   
    39.                     // Every thing looks ok - server nonce is fresh and nonce count seems to be   
    40.                     // incremented. Does not look like replay.  
    41.                     return true;  
    42.                 }  
    43.                      
    44.             }  
    45.         }  
    46.   
    47.         return false;  
    48.     }  
    49. }  
    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],如:

    C#代码 复制代码 收藏代码
    1. [Authorize]  
    2. public class ProductsController : ApiController  
    [Authorize]
    public class ProductsController : ApiController
    
     最后Global.asax里需注册下
    C#代码 复制代码 收藏代码
    1. GlobalConfiguration.Configuration.MessageHandlers.Add(  
    2. new AuthenticationHandler());  
    GlobalConfiguration.Configuration.MessageHandlers.Add(
    new AuthenticationHandler());
    
     
    三、客户端的调用
    这里主要说明使用WebClient调用
    C#代码 复制代码 收藏代码
    1. public static string Request(string sUrl, string sMethod, string sEntity, string sContentType,  
    2.     out string sMessage)  
    3. {  
    4.     try  
    5.     {  
    6.         sMessage = "";  
    7.         using (System.Net.WebClient client = new System.Net.WebClient())  
    8.         {  
    9.             client.Credentials = CreateAuthenticateValue(sUrl);  
    10.             client.Headers = CreateHeader(sContentType);  
    11.   
    12.             Uri url = new Uri(sUrl);  
    13.             byte[] bytes = Encoding.UTF8.GetBytes(sEntity);  
    14.             byte[] buffer;  
    15.             switch (sMethod.ToUpper())  
    16.             {  
    17.                 case "GET":  
    18.                     buffer = client.DownloadData(url);  
    19.                     break;  
    20.                 case "POST":  
    21.                     buffer = client.UploadData(url, "POST", bytes);  
    22.                     break;  
    23.                 default:  
    24.                     buffer = client.UploadData(url, "POST", bytes);  
    25.                     break;  
    26.             }  
    27.   
    28.             return Encoding.UTF8.GetString(buffer);  
    29.         }  
    30.     }  
    31.     catch (WebException ex)  
    32.     {  
    33.         sMessage = ex.Message;  
    34.         var rsp = ex.Response as HttpWebResponse;  
    35.         var httpStatusCode = rsp.StatusCode;  
    36.         var authenticate = rsp.Headers.Get("WWW-Authenticate");  
    37.   
    38.         return "";  
    39.     }  
    40.     catch (Exception ex)  
    41.     {  
    42.         sMessage = ex.Message;  
    43.         return "";  
    44.     }  
    45. }  
    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
    C#代码 复制代码 收藏代码
    1. private static CredentialCache CreateAuthenticateValue(string sUrl)  
    2. {  
    3.     CredentialCache credentialCache = new CredentialCache();  
    4.     credentialCache.Add(new Uri(sUrl), "Digest", new NetworkCredential("Lime", "Lime"));  
    5.   
    6.     return credentialCache;  
    7. }  
    private static CredentialCache CreateAuthenticateValue(string sUrl)
    {
        CredentialCache credentialCache = new CredentialCache();
        credentialCache.Add(new Uri(sUrl), "Digest", new NetworkCredential("Lime", "Lime"));
    
        return credentialCache;
    }
     至此整个认证就ok了。
  • 相关阅读:
    二维数组和指向指针的指针
    多路复用构建高性能服务器
    disque概要
    漫谈云计算与SOA (1)
    zeromq
    自定义内存分配
    基于行的操作
    反应器类型的操作
    多个流,简短的读和写
    缓存
  • 原文地址:https://www.cnblogs.com/zxh1919/p/7669997.html
Copyright © 2020-2023  润新知