最近的一个项目要求服务端与UI层分离,业务层以WebApi方式向外提供所有业务服务,服务在数据保密性方面提出了要求,主要包括:
1:客户端认证;
2:服务请求超时(默认5分钟);
3:服务Get请求的参数密文传输。
以上三个需求为一般的网络服务比较常见的简单要求,在WebApi项目中也比较容易实现,以下是我采用的两种实现方式(任意一种即可):
一:继承HttpClient来加入数据(Uri)加密,加入验证Header,继承ApiController来对Header进行合法性验证,并解密Uri
客户端的关键代码包括对HttpClient继承实现:
1 public class MyHttpClient : HttpClient 2 { 3 private static MyHttpClient _myClient; 4 5 public static MyHttpClient Instance 6 { 7 get 8 { 9 if(_myClient!=null)return _myClient; 10 _myClient = new MyHttpClient(); 11 _myClient.DefaultRequestHeaders.Add("Mac", MacMd5); 12 return _myClient; 13 } 14 } 15 16 private MyHttpClient() 17 { 18 } 19 20 private static readonly IAuthorize _authorize = new MacAuthorize(); 21 private static string _macMd5 = string.Empty; 22 23 private static string MacMd5 24 { 25 get 26 { 27 if (!string.IsNullOrEmpty(_macMd5)) return _macMd5; 28 var macAddress = _authorize.UniqueId; 29 if (string.IsNullOrEmpty(macAddress)) return string.Empty; 30 _macMd5 = macAddress.GetMD5(); 31 return _macMd5; 32 } 33 } 34 35 private static string TimeAes 36 { 37 get 38 { 39 var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 40 var aesTime = time.AesEncrypt(); 41 return aesTime; 42 } 43 } 44 45 private static void AddHeader(ref string url) 46 { 47 var segs = url.Split(‘/‘); 48 var para = segs[segs.Length - 1]; 49 if (para.Contains("=")) 50 { 51 var aes = para.AesEncrypt(); 52 url = url.Replace(para, aes.GetMD5()); 53 IEnumerable<string> p = new List<string>(); 54 if (_myClient.DefaultRequestHeaders.TryGetValues("Param", out p)) 55 _myClient.DefaultRequestHeaders.Remove("Param"); 56 _myClient.DefaultRequestHeaders.Add("Param",aes); 57 } 58 59 IEnumerable<string> time = new List<string>(); 60 if (_myClient.DefaultRequestHeaders.TryGetValues("Time", out time)) 61 _myClient.DefaultRequestHeaders.Remove("Time"); 62 _myClient.DefaultRequestHeaders.Add("Time", TimeAes); 63 } 64 65 public override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 66 System.Threading.CancellationToken cancellationToken) 67 { 68 var url = request.RequestUri.AbsoluteUri; 69 AddHeader(ref url); 70 return base.SendAsync(request, cancellationToken); 71 } 72 73 public override int GetHashCode() 74 { 75 return _myClient.GetHashCode(); 76 } 77 78 }
其中时间戳为了简便,未转为ms格式进行验证,仅作为字符串进行了验证。
在客户端进行调用时,通过继承的MyHttpClient发送服务请求即可:
1 var client = MyHttpClient.Instance; 2 var response = client.GetAsync(urlBase.AbsoluteUri + "api/test/GetUser/id=123123&name=jiakai").Result;
服务端的关键代码包括对ApiController的继承实现:
1 public class BaseApiController : ApiController 2 { 3 protected override void Initialize(HttpControllerContext controllerContext) 4 { 5 base.Initialize(controllerContext); 6 //获取请求头信息 7 var requestHeader = controllerContext.Request.Headers; 8 IEnumerable<string> mac = new List<string>(); 9 requestHeader.TryGetValues("Mac", out mac); 10 IEnumerable<string> time = new List<string>(); 11 requestHeader.TryGetValues("Time", out time); 12 IEnumerable<string> para = new List<string>(); 13 requestHeader.TryGetValues("Param", out para); 14 var paramList = controllerContext.Request.RequestUri.AbsoluteUri.Split(‘/‘); 15 if (mac == null || time == null || para == null) 16 { 17 var resp = Request.CreateResponse<string>(HttpStatusCode.Forbidden, "非法请求", "application/json"); 18 throw new HttpResponseException(resp); 19 } 20 //验证机器MAC地址 21 if (mac.Any()&&!string.IsNullOrEmpty(mac.First())) 22 { 23 //TODO:验证机器MAC地址是否为已注册MAC地址 24 } 25 //验证时间戳 26 if (time.Any() && !string.IsNullOrEmpty(time.First())) 27 { 28 var t = Convert.ToDateTime(time.First().AesDecrypt()); 29 if (t.AddMinutes(5) < DateTime.Now) 30 { 31 var resp = Request.CreateResponse<string>(HttpStatusCode.RequestTimeout, "请求过期", "application/json"); 32 throw new HttpResponseException(resp); 33 } 34 } 35 //验证参数MD5 36 if (para.Any() && !string.IsNullOrEmpty(para.First())) 37 { 38 var newMd5 = paramList[paramList.Length - 1]; 39 if (para.First().GetMD5() != newMd5) 40 { 41 var resp = Request.CreateResponse<string>(HttpStatusCode.Forbidden, "参数MD5验证失败", "application/json"); 42 throw new HttpResponseException(resp); 43 } 44 } 45 } 46 }
同样,在业务实现的Controller中,继承改为BaseApiController即可:
1 public class TestController : BaseApiController 2 { 3 //TODO:业务服务具体实现 4 }
二:通过对WebApi消息处理框架中HttpMessageHandler的自定义实现,并分别注入到Request及Response的消息处理阶段,来实现加密/解密的功能
客户端HttpClient中HttpMessageHandler的实现:
1 public class RequestHandler : DelegatingHandler 2 { 3 private static string TimeAes 4 { 5 get 6 { 7 var time = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); 8 var aesTime = time.AesEncrypt(); 9 return aesTime; 10 } 11 } 12 13 protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 14 System.Threading.CancellationToken cancellationToken) 15 { 16 var uri = request.RequestUri.AbsoluteUri; 17 var port = request.RequestUri.Port; 18 var para = uri.Split(‘/‘).Last(); 19 if (para.Contains("=")) 20 { 21 var aes = para.AesEncrypt(); 22 uri = uri.Replace(para, aes.GetMD5()); 23 IEnumerable<string> p = new List<string>(); 24 if (request.Headers.TryGetValues("Param", out p)) 25 request.Headers.Remove("Param"); 26 request.Headers.Add("Param", aes); 27 } 28 IEnumerable<string> time = new List<string>(); 29 if (request.Headers.TryGetValues("Time", out time)) 30 request.Headers.Remove("Time"); 31 request.Headers.Add("Time", TimeAes); 32 request.RequestUri = new Uri(uri); 33 34 return base.SendAsync(request, cancellationToken); 35 } 36 }
在实现了自定义的HttpMessageHandler后,实现一个HttpClient工厂,对外提供一个注入了该HttpMessageHandler的HttpClient单例对象:
1 public class AtmHttpClientFactory 2 { 3 private static HttpClient _atmClient; 4 private readonly static HttpClientHandler ClientHandler = new HttpClientHandler(); 5 6 public static HttpClient GetClient() 7 { 8 if (_atmClient != null) return _atmClient; 9 _atmClient = new HttpClient(new RequestHandler() {InnerHandler = ClientHandler }); 10 _atmClient.DefaultRequestHeaders.Add("Mac", MacMd5); 11 return _atmClient; 12 } 13 14 private AtmHttpClientFactory() 15 { 16 } 17 18 private static readonly IAuthorize _authorize = new MacAuthorize(); 19 private static string _macMd5 = String.Empty; 20 21 private static string MacMd5 22 { 23 get 24 { 25 if (!String.IsNullOrEmpty(_macMd5)) return _macMd5; 26 var macAddress = _authorize.UniqueId; 27 if (String.IsNullOrEmpty(macAddress)) return String.Empty; 28 _macMd5 = macAddress.GetMD5(); 29 return _macMd5; 30 } 31 } 32 }
这样,在客户端的调用加密过程就完全被封装,简化为如下:
1 //调用实现1 2 var myClient = AtmHttpClientFactory.GetClient(); 3 var myResponse = myClient.GetAsync("http://api.ezbatm.com:7777/api/test/GetUser/id=123123&name=jiakai").Result;
服务端也是类似的方法,在app的config中注入我们自己实现的服务端HttpMessageHandler即可。response的HttpMessageHandler实现:
1 public class ResponseHandler : DelegatingHandler 2 { 3 protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, 4 System.Threading.CancellationToken cancellationToken) 5 { 6 //获取请求头信息 7 IEnumerable<string> mac = new List<string>(); 8 request.Headers.TryGetValues("Mac", out mac); 9 IEnumerable<string> time = new List<string>(); 10 request.Headers.TryGetValues("Time", out time); 11 IEnumerable<string> para = new List<string>(); 12 request.Headers.TryGetValues("Param", out para); 13 var paramList = request.RequestUri.AbsoluteUri.Split(‘/‘); 14 //验证Header是否完整 15 if (mac == null || time == null || para == null) 16 { 17 return RequestNotAcceptable(); 18 } 19 //验证机器MAC地址 20 if (mac.Any() && !string.IsNullOrEmpty(mac.First())) 21 { 22 //TODO:验证机器MAC地址是否为已注册MAC地址 23 } 24 //验证时间戳 25 if (time.Any() && !string.IsNullOrEmpty(time.First())) 26 { 27 var t = Convert.ToDateTime(time.First().AesDecrypt()); 28 if (t.AddMinutes(5) < DateTime.Now) 29 { 30 return RequestTimeOut(); 31 } 32 } 33 //验证参数MD5 34 if (para.Any() && !string.IsNullOrEmpty(para.First())) 35 { 36 var newMd5 = paramList[paramList.Length - 1]; 37 //if (para.First().GetMD5() != newMd5) 38 { 39 return RequestForbidden(); 40 } 41 } 42 return base.SendAsync(request, cancellationToken); 43 } 44 45 private static Task<HttpResponseMessage> RequestForbidden() 46 { 47 return Task.Factory.StartNew(() => 48 { 49 return new HttpResponseMessage(HttpStatusCode.Forbidden) 50 { 51 Content = new StringContent("请求未通过认证") 52 }; 53 }); 54 } 55 56 private static Task<HttpResponseMessage> RequestTimeOut() 57 { 58 return Task.Factory.StartNew(() => 59 { 60 return new HttpResponseMessage(HttpStatusCode.RequestTimeout) 61 { 62 Content = new StringContent("请求已超时") 63 }; 64 }); 65 } 66 67 private static Task<HttpResponseMessage> RequestNotAcceptable() 68 { 69 return Task.Factory.StartNew(() => 70 { 71 return new HttpResponseMessage(HttpStatusCode.NotAcceptable) 72 { 73 Content = new StringContent("请求为非法请求") 74 }; 75 }); 76 } 77 }
之后,在(owin框架)Startup中注入该自定义HttpMessageHandler即可:
1 public void Configuration(IAppBuilder app) 2 { 3 // Configure Web API for self-host. 4 var config = new HttpConfiguration(); 5 config.Routes.MapHttpRoute( 6 name: "DefaultApi", 7 routeTemplate: "api/{controller}/{action}/{param}", 8 defaults: new { id = RouteParameter.Optional } 9 ); 10 //注入response handler 11 config.MessageHandlers.Add(new ResponseHandler()); 12 13 app.UseWebApi(config); 14 }
完成了以上客户端+服务器端的工作后,即可按照我们的实际需求对Request及Response消息进行自定义的处理。
总结:以上两种方法中,客户端及服务器端的方案都是解耦的,也就是是,你可以采用方法一的客户端方案+方法二的服务器端方案,等等以此类推。