最近使用WebApi開發一套對外接口,主要是數據的外送以及結果回傳,接口沒什麼難度,採用WebApi+EF的架構簡單建立一個模板工程,使用template生成一套WebApi接口,去掉put、delete等操做,修改一下就能夠上線。這些都不在話下,反正網上一大堆教程,隨便找那個step by step作下來就能夠了。html
而後發佈上線後,接口是放在外網,面臨兩個問題:web
其實這兩個問題是相互結合的,先保證合法,而後在合法基礎上保證請求的惟一性,避免參數被篡改。算法
鑑於接口上線期限緊迫,結合衆多案例,先解決掉接口調用數據的安全性問題,這裏採用了RSA報文加解密的方案,保證數據安全和防止接口被惡意調用以及參數篡改的問題。api
本文參考博客園多篇博文,內容多有引用,文末附有參照博文的地址。數組
如下爲正文!安全
這裏參照了WebAPi使用公鑰私鑰加密介紹和使用 一文,進行公鑰私鑰加解密的處理服務器
先說服務端:微信
先看一下MessageProcessingHandler的介紹:架構
#region 程序集 System.Net.Http, Version=4.2.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a // C:\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Net.Http.dll #endregion using System.Threading; using System.Threading.Tasks; namespace System.Net.Http { // // 摘要: // 僅對請求和/或響應消息進行一些小型處理的處理程序的基類。 public abstract class MessageProcessingHandler : DelegatingHandler { // // 摘要: // 建立的一個實例 System.Net.Http.MessageProcessingHandler 類。 protected MessageProcessingHandler(); // // 摘要: // 建立的一個實例 System.Net.Http.MessageProcessingHandler 具備特定的內部處理程序類。 // // 參數: // innerHandler: // 內部處理程序負責處理 HTTP 響應消息。 protected MessageProcessingHandler(HttpMessageHandler innerHandler); // // 摘要: // 處理每一個發送到服務器的請求。 // // 參數: // request: // 要處理的 HTTP 請求消息。 // // cancellationToken: // 可由其餘對象或線程用以接收取消通知的取消標記。 // // 返回結果: // 已處理的 HTTP 請求消息。 protected abstract HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken); // // 摘要: // 處理來自服務器的每一個響應。 // // 參數: // response: // 要處理的 HTTP 響應消息。 // // cancellationToken: // 可由其餘對象或線程用以接收取消通知的取消標記。 // // 返回結果: // 已處理的 HTTP 響應消息。 protected abstract HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken); // // 摘要: // 異步發送 HTTP 請求到要發送到服務器的內部處理程序。 // // 參數: // request: // 要發送到服務器的 HTTP 請求消息。 // // cancellationToken: // 可由其餘對象或線程用以接收取消通知的取消標記。 // // 返回結果: // 表示異步操做的任務對象。 // // 異常: // T:System.ArgumentNullException: // request 是 null。 protected internal sealed override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken); } }
擴展這個類的目的是解密參數,其實也能夠推遲到Action過濾器中作,可是仍是以爲時機上在這裏處理比較合適。具體的建議瞭解一下WebApi消息管道以及擴展過濾器的相關文章,本文再也不延伸。異步
下面是擴展的實現代碼:
/// <summary> /// 請求預處理,報文解密 /// </summary> /// <seealso cref="System.Net.Http.MessageProcessingHandler"/> public class ArgDecryptMessageProcesssingHandler : MessageProcessingHandler { /// <summary> /// 處理每一個發送到服務器的請求。 /// </summary> /// <param name="request"> 要處理的 HTTP 請求消息。</param> /// <param name="cancellationToken">可由其餘對象或線程用以接收取消通知的取消標記。</param> /// <returns>已處理的 HTTP 請求消息。</returns> protected override HttpRequestMessage ProcessRequest(HttpRequestMessage request, CancellationToken cancellationToken) { var contentType = request.Content.Headers.ContentType; //swagger請求直接跳過不予處理 if (request.RequestUri.AbsolutePath.Contains("/swagger")) { return request; } //得到平臺私鑰 string privateKey = Common.GetRsaPrivateKey(); //獲取Get中的Query信息,解密後重置請求上下文 if (request.Method == HttpMethod.Get) { string baseQuery = request.RequestUri.Query; if (!string.IsNullOrEmpty(baseQuery)) { baseQuery = baseQuery.Substring(1); baseQuery = Regex.Match(baseQuery, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value; baseQuery = RsaHelper.RSADecrypt(privateKey, baseQuery); var requestUrl = $"{request.RequestUri.AbsoluteUri.Split('?')[0]}?{baseQuery}"; request.RequestUri = new Uri(requestUrl); } } //獲取Post請求中body中的報文信息,解密後重置請求上下文 if (request.Method == HttpMethod.Post) { string baseContent = request.Content.ReadAsStringAsync().Result; baseContent = Regex.Match(baseContent, "(sign=)*(?<sign>[\\S]+)").Groups[2].Value; baseContent = RsaHelper.RSADecrypt(privateKey, baseContent); request.Content = new StringContent(baseContent); //此contentType必須最後設置 不然會變成默認值 request.Content.Headers.ContentType = contentType; } return request; } /// <summary> /// 處理來自服務器的每一個響應。 /// </summary> /// <param name="response"> 要處理的 HTTP 響應消息。</param> /// <param name="cancellationToken">可由其餘對象或線程用以接收取消通知的取消標記。</param> /// <returns>已處理的 HTTP 響應消息。</returns> protected override HttpResponseMessage ProcessResponse(HttpResponseMessage response, CancellationToken cancellationToken) { return response; } }
獲取平臺私鑰那裏,實際上能夠針對不一樣的接口調用方單獨一個,另起一篇在介紹。
而後找到解決方案【App_Start】目錄下的WebApiConfig類,在裏面添加以下代碼,啓用消息處理擴展類:
public static void Register(HttpConfiguration config) { // Web API 路由 config.MapHttpAttributeRoutes(); config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); config.MessageHandlers.Add(new ArgDecryptMessageProcesssingHandler()); }
注意!注意!注意!
原博文中是擴展的 AuthorizeAttribute,即認證和受權過濾器,代碼實現上是沒有多大差異的;在時機上認證和受權過濾器要比方法過濾器執行的要早,更適合作認證和受權的操做。而咱們擴展這個過濾器的目的是對報文進行簽名驗證以及超時驗證,因此使用方法過濾器更恰當些。
下面是擴展過濾器的代碼:
/// <summary> /// 擴展方法過濾器,進入方法前驗證簽名 /// </summary> public class ApiVerifyFilter : ActionFilterAttribute { public override void OnActionExecuting(HttpActionContext actionContext) { base.OnActionExecuting(actionContext); //獲取平臺私鑰 string privateKey = Common.GetRsaPrivateKey(); //獲取請求的超時時間,爲了測試設置爲100秒,即兩次調用間隔不能超過100秒 string expireyTime = ConfigurationManager.AppSettings["UrlExpireTime"]; var request = actionContext.Request; //驗證簽名所需header內容 if (!request.Headers.Contains("signature") || !request.Headers.Contains("timestamp") || !request.Headers.Contains("nonce")) { SetSpecialResponseMessage(actionContext, 40301); return; } var token = string.Empty; var signature = request.Headers.GetValues("signature").FirstOrDefault(); var timeStamp = request.Headers.GetValues("timestamp").FirstOrDefault(); var nonce = request.Headers.GetValues("nonce").FirstOrDefault(); //驗證簽名 if (!Common.SignValidate(privateKey, nonce, timeStamp, signature, token)) { SetSpecialResponseMessage(actionContext, 40302); return; } //檢查接口調用是否超時 var ts = Common.DateTime2TimeStamp(DateTime.UtcNow) - Convert.ToDouble(timeStamp); if (ts > int.Parse(expireyTime) * 1000) { SetSpecialResponseMessage(actionContext, 40303); return; } } /// <summary> /// 設置簽名驗證異常返回狀態 /// </summary> /// <param name="actionContext">當前請求上下文</param> /// <param name="statusCode">異常狀態碼</param> private static void SetSpecialResponseMessage(HttpActionContext actionContext, int statusCode) { BizResponseModel model = new BizResponseModel { Status = statusCode, Date = DateTime.Now.ToString("yyyyMMddhhmmssfff"), Message = "服務端拒絕訪問" }; switch (statusCode) { case 40301: model.Message = "沒有設置簽名、時間戳、隨機字符串"; break; case 40302: model.Message = "簽名無效"; break; case 40303: model.Message = "無效的請求"; break; default: break; } actionContext.Response = new HttpResponseMessage { Content = new StringContent(JsonConvert.SerializeObject(model)) }; } public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext) { base.OnActionExecuted(actionExecutedContext); } }
/// <summary> /// 特殊狀態 /// </summary> public class BizResponseModel { public int Status { get; set; } public string Message { get; set; } public string Date { get; set; } }
而後下面是用的公共方法:
/// <summary> /// 獲取時間戳毫秒數 /// </summary> /// <param name="dateTime"></param> /// <returns></returns> public static long DateTime2TimeStamp(DateTime dateTime) { TimeSpan ts = DateTime.UtcNow - new DateTime(1970, 1, 1, 0, 0, 0, 0); return Convert.ToInt64(ts.TotalMilliseconds); } public static bool SignValidate(string privateKey, string nonce, string timestamp, string signature, string token) { bool isValidate = false; var tempSign = RsaHelper.RSADecrypt(privateKey, signature); string[] arr = new[] { token, timestamp, nonce }.OrderBy(z => z).ToArray(); string arrString = string.Join("", arr); var sha256Result = arrString.EncryptSha256(); if (sha256Result == tempSign) { isValidate = true; } return isValidate; }
簽名驗證的過程以下:
而後,如今須要啓用剛添加的方法過濾器,由於是繼承與屬性,能夠全局啓用,或者單個Controller中啓用、或者爲某個Action啓用。全局啓用代碼以下:
下的WebApiConfig類添加以下代碼:
config.Filters.Add(new ApiVerifyFilter());
OK,所有完成,最後附上兩個先後的效果對比!
參考博文:
Asp.Net WebAPI中Filter過濾器的使用以及執行順序
寫博文太累了,回家吃螃蟹補補~