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,关于签名认证就先到这里吧,如有不对之处,欢迎讨论。

posted @ 2018-04-04 00:19  Abeam  阅读(800)  评论(0编辑  收藏  举报