Ø  前言

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

1.   什么是签名认证

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

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

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

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

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

2.   应用场景

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

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 全局异常过滤器无法捕获,所以需要加上 try、catch 自行处理异常

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=1分40秒

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=1分40秒

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 方式调用(失败)

2)   GET 方式调用(成功)

3)   POST 方式调用(失败)

4)   POST 方式调用(成功)

Ø  总结

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

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

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

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

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

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

ASP.NET Web API 2 使用 DelegatingHandler(委托处理程序)实现签名认证的更多相关文章

  1. ASP.NET Web API 安全验证之摘要(Digest)认证

    在基本认证的方式中,主要的安全问题来自于用户信息的明文传输,而在摘要认证中,主要通过一些手段避免了此问题,大大增加了安全性. 1.客户端匿名的方式请求 (无认证) HTTP/ Unauthorized ...

  2. ASP.NET Web API标准的“管道式”设计

    ASP.NET Web API的核心框架是一个消息处理管道,这个管道是一组HttpMessageHandler的有序组合.这是一个双工管道,请求消息从一端流入并依次经过所有HttpMessageHan ...

  3. Asp.Net Web API 2第四课——HttpClient消息处理器

    Asp.Net Web API 导航   Asp.Net Web API第一课:入门http://www.cnblogs.com/aehyok/p/3432158.html Asp.Net Web A ...

  4. 【ASP.NET Web API教程】5.1 HTTP消息处理器

    原文:[ASP.NET Web API教程]5.1 HTTP消息处理器 注:本文是[ASP.NET Web API系列教程]的一部分,如果您是第一次看本系列教程,请先看前面的内容. 5.1 HTTP ...

  5. 【ASP.NET Web API教程】3.4 HttpClient消息处理器

    原文:[ASP.NET Web API教程]3.4 HttpClient消息处理器 注:本文是[ASP.NET Web API系列教程]的一部分,如果您是第一次看本博客文章,请先看前面的内容. 3.4 ...

  6. ASP.NET Web API 2 消息处理管道

    Ø  前言 ASP.NET 的应用程序都会有自己的消息处理管道和生命周期,比如:ASP.NET Web 应用程序(Web Form).ASP.NET MVC,还有本文将讨论的 ASP.NET Web ...

  7. ASP.NET Web API 框架研究 服务容器 ServicesContainer

    ServicesContainer是一个服务的容器,可以理解为—个轻量级的IoC容器,其维护着一个服务接口类型与服务实例之间的映射关系,可以根据服务接口类型获取对应的服务实例.构成ASP.NET We ...

  8. ASP.NET Web API 记录请求响应数据到日志的一个方法

    原文:http://blog.bossma.cn/dotnet/asp-net-web-api-log-request-response/ ASP.NET Web API 记录请求响应数据到日志的一个 ...

  9. 适用于app.config与web.config的ConfigUtil读写工具类 基于MongoDb官方C#驱动封装MongoDbCsharpHelper类(CRUD类) 基于ASP.NET WEB API实现分布式数据访问中间层(提供对数据库的CRUD) C# 实现AOP 的几种常见方式

    适用于app.config与web.config的ConfigUtil读写工具类   之前文章:<两种读写配置文件的方案(app.config与web.config通用)>,现在重新整理一 ...

随机推荐

  1. CCCC 喊山

    2016年天梯赛模拟&初赛题集(nwu) 编程题30小题,共计580分 580分 编程题 5-14 喊山   (30分) 喊山,是人双手围在嘴边成喇叭状,对着远方高山发出“喂—喂喂—喂喂喂…… ...

  2. luogu4159 迷路 (矩阵加速)

    考虑如果只有距离为1的边,那我用在时间i到达某个点的状态数矩阵 乘上转移矩阵(就是边的邻接矩阵),就能得到i+1时间的 然后又考虑到边权只有1~9,那可以把边拆成只有距离为1的 具体做法是一个点拆成9 ...

  3. [ZJOI2012]灾难(建图)

    阿米巴是小强的好朋友. 阿米巴和小强在草原上捉蚂蚱.小强突然想,如果蚂蚱被他们捉灭绝了,那么吃蚂蚱的小鸟就会饿死,而捕食小鸟的猛禽也会跟着灭绝,从而引发一系列的生态灾难. 学过生物的阿米巴告诉小强,草 ...

  4. 一个GD初二蒟蒻的自我介绍

    emmm……今天博客第一天使用呢,好激动啊…… 这里是一个来自GD的初二蒟蒻+无脑OIER,什么都不会 NOIP2017普及组:260压线1=还是看RP过的…… GDKOI2018:120暴力大法吼啊 ...

  5. centos7/rhel7下安装redis4.0集群

    相关介绍:Redis从3.0版本开始支持集群! 集群一般由多个节点组成,节点数量至少6个才能保证组成完整高可用的集群. 每个节点需要开启配置文件中的cluster-enabled yes,让Redis ...

  6. hdu 1907 (尼姆博弈)

    题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=1907 Problem Description Little John is playing very ...

  7. mysql 远程连接 10038

    1,先确认本地是否能连上本地能连上就对用户进行授权 mysql>grant all privileges on *.* to 'root'@'%' identified by 'youpassw ...

  8. mysql connections

    在使用MySQL数据库的时候,经常会遇到这么一个问题,就是“Can not connect to MySQL server. Too many connections”-mysql 1040错误,这是 ...

  9. ArcGIS for qml - 地址地标转换为经纬度(地理编码)

    实现输入地址地标转换为其经纬度 本文链接:地理编码 作者: 狐狸家的鱼 Github: 八至 一.地理编码 1.地理编码含义 地址编码(或地理编码)是使用地址中包含的信息来插入地图上的相应位置的过程. ...

  10. JAVA概述 也许你会豁然开朗

    1.JDK:Java Development Kit,java的开发和运行环境,java的开发工具和jre. 2.JRE:Java Runtime Environment,java程序的运行环境,ja ...