zoukankan      html  css  js  c++  java
  • ASP.NET Web API 2 使用 DelegatingHandler(委托处理程序)实现签名认证

    Ø  前言

    在前一篇ASP.NET Web API 2 使用 AuthorizationFilter(授权过滤器)实现 Basic 认证文章中实现了采用 Basic 认证的方式,但是这种方式存在安全隐患,而且只适合同一体系的项目架构中。如果希望将接口对外发布,提供给其他应用程序或其他语言调用,就需要具有更高的安全性,这就是本文需要讨论话题了。

     

    1.   什么是签名认证

    签名认证采用了可靠的加密机制对请求进行验证,提高了接口的安全性,防止数据篡改的现象。下面是具体实现:

    1)   首先,接口提供者需要提供 AppId SecretKey 给调用者,只有获得的了 AppId SecretKey 的调用者才有权限访问。

    2)   调用者在调用接口之前,需要根据提供者给出签名方案对请求数据进行加密,通常采用不可逆的加密方式,例如:MD5HMAC等。

    3)   提供者收到请求后,同样以相同的签名方案对请求数据进行加密,再将加密的结果与调用者的加密结果进行比较,如果两者相同则认为签名成功允许继续访问,否则拒绝请求。

    4)   注意:加密过程中,SecretKey 禁止被传递,通常作为密钥使用,因为就算 AppId 被截取,也不能正常签名。

     

    2.   应用场景

    clip_image002[8]

    从图中可以看出,无论是那种程序调用接口,都必须采用相同的签名方案才能正常调用,否则请求将会被拒绝。

     

    3.   具体实现步骤

    1)   首先,创建一个签名认证处理程序

    /// <summary>

    /// 签名认证处理程序。

    /// </summary>

    public class SignatureAuthenticationHandler : System.Net.Http.DelegatingHandler

    {

        protected override async System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage> SendAsync(

            System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken)

        {

            //因为 DelegatingHandler 全局异常过滤器无法捕获,所以需要加上 trycatch 自行处理异常

            try

            {

                //1. URL 编码

                string encodeUrl = HttpUtility.UrlEncode(request.RequestUri.AbsoluteUri);

     

                //2. 请求内容 MD5 加密,再 base64 编码

                string reqContent = string.Empty;

                using (var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider())

                {

                    byte[] reqBytes = null;

                    if (request.Method == HttpMethod.Get)

                        reqBytes = Encoding.UTF8.GetBytes(request.RequestUri.Query);

                    else

                        reqBytes = request.Content.ReadAsByteArrayAsync().Result;

                    byte[] encryptBytes = md5.ComputeHash(reqBytes);

                    reqContent = Convert.ToBase64String(encryptBytes);

                }

     

                //获取 Authorization

                var auth = request.Headers.Authorization;

                if (auth == null)

                    return request.CreateResponse(HttpStatusCode.Forbidden, "未指定 Authorization");

                else if (!"hmac".Equals(auth.Scheme, StringComparison.CurrentCultureIgnoreCase))

                    return request.CreateResponse(HttpStatusCode.Forbidden, "未指定 hmac");

                else if (string.IsNullOrWhiteSpace(auth.Parameter))

                    return request.CreateResponse(HttpStatusCode.Forbidden, "无签名参数");

                string[] paras = auth.Parameter.Split('&');

                if (paras.Length != 4)

                    return request.CreateResponse(HttpStatusCode.Forbidden, "签名参数无效");

     

                //检查AppId,真实情况下:应该是根据请求的 AppId 查出对应的合作伙伴记录

                var partners = new[]

                {

                    new { AppId = "zhangsan", SecretKey = "abc12345" },

                    new { AppId = "lisi", SecretKey = "def45678" }

                };  //模拟真实数据

                var partner = partners.FirstOrDefault(o => o.AppId == paras[0]);

                if (partner == null)

                    return request.CreateResponse(HttpStatusCode.Forbidden, "AppId 无效");

     

                //3. AppId、时间戳、Guid、编码的 URL、请求内容串联

                string signStr = string.Format("{0}{1}{2}{3}{4}",

                    paras[0], paras[1], paras[2], encodeUrl, reqContent);

     

                //4. 使用 SecretKey 作为密钥,将串联的字符串进行散列算法加密,转为大写的十六进制字符串

                StringBuilder sbHex = new StringBuilder();

                byte[] secretKeyBytes = Convert.FromBase64String(partner.SecretKey);

                using (var hmac = new System.Security.Cryptography.HMACSHA512(secretKeyBytes))

                {

                    byte[] bytes = Encoding.UTF8.GetBytes(signStr);

                    byte[] encryptBytes = hmac.ComputeHash(bytes);

                    foreach (var item in encryptBytes)

                    {

                        sbHex.AppendFormat("{0:X2}", item);

                    }

                }

     

                //5. 比较签名

                string sign = sbHex.ToString();

                if (paras[3].Equals(sign))

                    return base.SendAsync(request, cancellationToken).Result;

                else

                    return request.CreateResponse(HttpStatusCode.Unauthorized, "已拒绝为此请求授权");

            }

            catch (Exception ex)

            {

                return request.CreateResponse(HttpStatusCode.InternalServerError, string.Format("签名验证出错:{0}", ex.Message));

            }

        }

    }

     

    2)   添加签名认证处理程序至信息管道中,在 WebApiConfig.cs 中添加代码

    config.MessageHandlers.Add(new WebAPI2.Filter.MessageHandlers.SignatureAuthenticationHandler());

     

    3)   然后,再建一个单元测试项目(用于模拟调用接口,分别使用 GET POST

    [TestClass]

    public class WebApiTest

    {

        /// <summary>

        /// 获取时间戳(毫秒)。

        /// </summary>

        /// <returns></returns>

        public long GetMillisecondsTimestamp(DateTime dateTime)

        {

            TimeSpan cha = (dateTime - TimeZone.CurrentTimeZone.ToLocalTime(new DateTime(1970, 1, 1)));

            return (long)cha.TotalMilliseconds;

        }

     

        [TestMethod]

        public void TestSAGet()

        {

            //1. URL 编码

            Uri uri = new Uri("http://localhost:37907/api/customer/get?id=1");

            string encodeUrl = HttpUtility.UrlEncode(uri.AbsoluteUri);

     

            //2. 请求内容 MD5 加密,再 base64 编码

            string reqContent = string.Empty;

            using (var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider())

            {

                //GET方式:将?id=1 加密

                byte[] reqBytes = Encoding.UTF8.GetBytes(uri.Query);

                byte[] encryptBytes = md5.ComputeHash(reqBytes);

                reqContent = Convert.ToBase64String(encryptBytes);

            }

     

            //3. AppId、时间戳、Guid、编码的 URL、请求内容串联

            string appId = "zhangsan";

            long timestamp = GetMillisecondsTimestamp(DateTime.Now);

            string guid = Guid.NewGuid().ToString();

            string signStr = string.Format("{0}{1}{2}{3}{4}",

                appId, timestamp, guid, encodeUrl, reqContent);

     

            //4. 使用 SecretKey 作为密钥,将串联的字符串进行散列算法加密,转为大写的十六进制字符串

            StringBuilder sbHex = new StringBuilder();

            string secretKey = "abc12346";

            byte[] secretKeyBytes = Convert.FromBase64String(secretKey);

            using (var hmac = new System.Security.Cryptography.HMACSHA512(secretKeyBytes))

            {

                byte[] bytes = Encoding.UTF8.GetBytes(signStr);

                byte[] encryptBytes = hmac.ComputeHash(bytes);

                foreach (var item in encryptBytes)

                {

                    sbHex.AppendFormat("{0:X2}", item);

                }

            }

            string sign = sbHex.ToString();

     

            WebRequest request = HttpWebRequest.Create(HttpUtility.UrlDecode(encodeUrl));

            request.Timeout = 200000;   //默认为100000=140

            request.Method = "GET";

     

            //5. AppId、时间戳、Guid、加密后的字符串以&号连接,并以请求头 Authorization 传递(验证方案为 hmac

            request.Headers.Add(HttpRequestHeader.Authorization,

                string.Format("hmac {0}&{1}&{2}&{3}", appId, timestamp, guid, sign));

            try

            {

                using (WebResponse response = request.GetResponse())

                {

                    using (StreamReader sr = new StreamReader(response.GetResponseStream()))

                    {

                        string resCon = sr.ReadToEnd();

                    }

                }

            }

            catch (WebException ex)

            {

                HttpWebResponse webResponse = ex.Response as HttpWebResponse;

                using (StreamReader sr = new StreamReader(webResponse.GetResponseStream()))

                {

                    int status = (int)webResponse.StatusCode;

                    string resCon = sr.ReadToEnd();

                    throw ex;

                }

            }

        }

     

        [TestMethod]

        public void TestSAPost()

        {

            var cust = new { CustomerId = 1, CustomerName = "客户A", Address = "上海市杨浦区" };

            string data = JsonConvert.SerializeObject(cust);

            byte[] reqBytes = Encoding.UTF8.GetBytes(data);

     

            //1. URL 编码

            Uri uri = new Uri("http://localhost:37907/api/customer/modify");

            string encodeUrl = HttpUtility.UrlEncode(uri.AbsoluteUri);

     

            //2. 请求内容 MD5 加密,再 base64 编码

            string reqContent = string.Empty;

            using (var md5 = new System.Security.Cryptography.MD5CryptoServiceProvider())

            {

                //POST方式:将请求数据加密

                byte[] encryptBytes = md5.ComputeHash(reqBytes);

                reqContent = Convert.ToBase64String(encryptBytes);

            }

     

            //3. AppId、时间戳、Guid、编码的 URL、请求内容串联

            string appId = "zhangsan";

            long timestamp = GetMillisecondsTimestamp(DateTime.Now);

            string guid = Guid.NewGuid().ToString();

            string signStr = string.Format("{0}{1}{2}{3}{4}",

                appId, timestamp, guid, encodeUrl, reqContent);

     

            //4. 使用 SecretKey 作为密钥,将串联的字符串进行散列算法加密,转为大写的十六进制字符串

            StringBuilder sbHex = new StringBuilder();

            string secretKey = "abc12346";

            byte[] secretKeyBytes = Convert.FromBase64String(secretKey);

            using (var hmac = new System.Security.Cryptography.HMACSHA512(secretKeyBytes))

            {

                byte[] bytes = Encoding.UTF8.GetBytes(signStr);

                byte[] encryptBytes = hmac.ComputeHash(bytes);

                foreach (var item in encryptBytes)

                {

                    sbHex.AppendFormat("{0:X2}", item);

                }

            }

            string sign = sbHex.ToString();

     

            WebRequest request = HttpWebRequest.Create(HttpUtility.UrlDecode(encodeUrl));

            request.Timeout = 200000;   //默认为100000=140

            request.Method = "POST";

            request.ContentType = "application/json";

            request.ContentLength = reqBytes.Length;

     

            //5. AppId、时间戳、Guid、加密后的字符串以&号连接,并以请求头 Authorization 传递(验证方案为 hmac

            request.Headers.Add(HttpRequestHeader.Authorization,

                string.Format("hmac {0}&{1}&{2}&{3}", appId, timestamp, guid, sign));

            try

            {

                using (Stream stream = request.GetRequestStream())

                {

                    stream.Write(reqBytes, 0, reqBytes.Length);

                    using (WebResponse response = request.GetResponse())

                    {

                        using (StreamReader sr = new StreamReader(response.GetResponseStream()))

                        {

                            string resCon = sr.ReadToEnd();

                        }

                    }

                }

            }

            catch (WebException ex)

            {

                HttpWebResponse webResponse = ex.Response as HttpWebResponse;

                using (StreamReader sr = new StreamReader(webResponse.GetResponseStream()))

                {

                    int status = (int)webResponse.StatusCode;

                    string resCon = sr.ReadToEnd();

                    throw ex;

                }

            }

        }

    }

     

    4.   模拟调用效果

    1)   GET 方式调用(失败)

    clip_image003[10]

     

    2)   GET 方式调用(成功)

    clip_image004[7]

     

    3)   POST 方式调用(失败)

    clip_image005[4]

     

    4)   POST 方式调用(成功)

    clip_image006[4]

     

    Ø  总结

    1.   本文主要阐述了如何使用 Web API 实现签名认证,以及实现步骤和效果演示。

    2.   不难发现,其实签名认证也是存在一定缺点的,比如:

    1)   每次请求都需要将做同一件事(将请求签名),而服务端接收到请求后也是每次都要验证,提高了复杂程度和性能损耗。

    2)   服务端收到请求时还需要根据 AppId 查找对应的 SecretKey,无论是在数据库中查找还是从缓存中获取,这无疑也是对性能的开销。因为当访问量大时,这种频繁的操作也是不能忽视的。

    3.   当然,有缺点也有优点的,这样做最大的好处就是提高了接口的安全性。

    4.   OK,关于签名认证就先到这里吧,如有不对之处,欢迎讨论。

  • 相关阅读:
    cef加载flash的办法
    一个高性能的对象属性复制类,支持不同类型对象间复制,支持Nullable<T>类型属性
    php检测php.ini是否配制正确
    openwrt的路由器重置root密码
    windows 7 + vs2010 sp1编译 x64位版qt4
    解决SourceGrid在某些系统上无法用鼠标滚轮滚动的问题
    判断一个点是否在多边形内部,射线法思路,C#实现
    [转载]使用HttpWebRequest进行请求时发生错误:基础连接已关闭,发送时发生错误处理
    让Dapper+SqlCE支持ntext数据类型和超过4000字符的存储
    通过WMI
  • 原文地址:https://www.cnblogs.com/abeam/p/8712216.html
Copyright © 2011-2022 走看看