ASP.NET Web API 2 使用 DelegatingHandler(委托处理程序)实现签名认证
Ø 前言
在前一篇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(委托处理程序)实现签名认证的更多相关文章
- ASP.NET Web API 安全验证之摘要(Digest)认证
在基本认证的方式中,主要的安全问题来自于用户信息的明文传输,而在摘要认证中,主要通过一些手段避免了此问题,大大增加了安全性. 1.客户端匿名的方式请求 (无认证) HTTP/ Unauthorized ...
- ASP.NET Web API标准的“管道式”设计
ASP.NET Web API的核心框架是一个消息处理管道,这个管道是一组HttpMessageHandler的有序组合.这是一个双工管道,请求消息从一端流入并依次经过所有HttpMessageHan ...
- 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 ...
- 【ASP.NET Web API教程】5.1 HTTP消息处理器
原文:[ASP.NET Web API教程]5.1 HTTP消息处理器 注:本文是[ASP.NET Web API系列教程]的一部分,如果您是第一次看本系列教程,请先看前面的内容. 5.1 HTTP ...
- 【ASP.NET Web API教程】3.4 HttpClient消息处理器
原文:[ASP.NET Web API教程]3.4 HttpClient消息处理器 注:本文是[ASP.NET Web API系列教程]的一部分,如果您是第一次看本博客文章,请先看前面的内容. 3.4 ...
- ASP.NET Web API 2 消息处理管道
Ø 前言 ASP.NET 的应用程序都会有自己的消息处理管道和生命周期,比如:ASP.NET Web 应用程序(Web Form).ASP.NET MVC,还有本文将讨论的 ASP.NET Web ...
- ASP.NET Web API 框架研究 服务容器 ServicesContainer
ServicesContainer是一个服务的容器,可以理解为—个轻量级的IoC容器,其维护着一个服务接口类型与服务实例之间的映射关系,可以根据服务接口类型获取对应的服务实例.构成ASP.NET We ...
- ASP.NET Web API 记录请求响应数据到日志的一个方法
原文:http://blog.bossma.cn/dotnet/asp-net-web-api-log-request-response/ ASP.NET Web API 记录请求响应数据到日志的一个 ...
- 适用于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通用)>,现在重新整理一 ...
随机推荐
- VLAN报文和非VLAN以太网报文的区别
VLAN(Virtual Local Area Network,虚拟局域网)协议,基于802.1Q协议标准. 以太网带VLAN帧结构,是在以太网报文中,位于数据帧中“发送源MAC地址”与“类别/长度域 ...
- [SCOI2008]奖励关(期望dp)
你正在玩你最喜欢的电子游戏,并且刚刚进入一个奖励关.在这个奖励关里,系统将依次随机抛出k次宝物,每次你都可以选择吃或者不吃(必须在抛出下一个宝物之前做出选择,且现在决定不吃的宝物以后也不能再吃). 宝 ...
- Http协议常见状态码
206 - 断点下载时用到,客户端请求了一部分内容,服务器成功把这部分内容返回给它,这时候就是用这个状态. 301 - 永久跳转,原地址不存在了,url被指向到另一个地址.这个主要是搜索引擎相关,影响 ...
- poj3190 Stall Reservations
我一开始想用线段树,但是发现还要记录每头cow所在的棚...... 无奈之下选择正解:贪心. 用priority_queue来维护所有牛棚中结束时间最早的那个牛棚,即可得出答案. 注意代码实现的细节. ...
- 洛谷P1712 区间
题意:给你n个区间,从中选择m个,使得它们有交,且最长与最短区间的差值最小. 解:这道题我想了好多的,nlog²n错的,nlogn错的,最后终于想出nlogn的了...... 把区间按照长度排序,然后 ...
- tfs 2013 利用 web deploy 完成asp.net站点自动发布
课题起因: 目前我们团队使用visual studio 2013开发asp.net项目, 使用tfs2013 做源码管理, 每天早上手动发布项目文件包,复制到测试服务器的站点文件夹下覆盖老文件,用此方 ...
- django orm 重点大全
1.最简单的跨表,查询外键表中符合主表条件的记录列表 #用户类型表 class User_typ(models.Model): name=models.CharField(max_length=32) ...
- JS 判断各种设备,各种浏览器
话不多说,直接看代码 1.区分Android.iphone.ipad: var ua = navigator.userAgent.toLowerCase(); if (/android|adr/gi. ...
- spring整合redis连接
两种连接方式:(写了一半,未测试) spring xml: <?xml version="1.0" encoding="UTF-8"?> <b ...
- TestNg 6.异常测试
* 什么时候会用到异常测试??* 在我们期望结果为某一个异常的时候* 比如:我们传入了某些不合法的参数,程序抛出异常* 也就是我的预期结果就是这个异常看以下的一段代码: package com.cou ...