背景介紹 最近使用WebApi開發一套對外介面,主要是數據的外送以及結果回傳,介面沒什麼難度,採用WebApi+EF的架構簡單創建一個模板工程,使用template生成一套WebApi介面,去掉put、delete等操作,修改一下就可以上線。這些都不在話下,反正網上一大堆教程,隨便找那個step b... ...
背景介紹
最近使用WebApi開發一套對外介面,主要是數據的外送以及結果回傳,介面沒什麼難度,採用WebApi+EF的架構簡單創建一個模板工程,使用template生成一套WebApi介面,去掉put、delete等操作,修改一下就可以上線。這些都不在話下,反正網上一大堆教程,隨便找那個step by step做下來就可以了。
然後發佈上線後,介面是放在外網,面臨兩個問題:
- 如何保證介面的調用的合法性
- 如何保證介面及數據的安全性
其實這兩個問題是相互結合的,先保證合法,然後在合法基礎上保證請求的唯一性,避免參數被篡改。
鑒於介面上線期限緊迫,結合眾多案例,先解決掉介面調用數據的安全性問題,這裡採用了RSA報文加解密的方案,保證數據安全和防止介面被惡意調用以及參數篡改的問題。
本文參考博客園多篇博文,內容多有引用,文末附有參照博文的地址。
以下為正文!
正文
首先,介面面臨的問題:
請求來源(身份)是否合法(部分解決,後續在處理)?- 請求參數被篡改?
- 請求的唯一性(不可複製),防止請求被惡意攻擊
解決方案:
- 參數加密: 客戶端和服務端參數採用RSA加密後傳遞,原則上只有持有私鑰的服務端才能解密客戶端公鑰加密的參數,避免了參數篡改的問題
- 請求簽名:採用一套簽名演算法,對請求進行簽名驗證,保證請求的唯一性
這裡參照了WebAPi使用公鑰私鑰加密介紹和使用 一文,進行公鑰私鑰加解密的處理
先說服務端:
擴展 MessageProcessingHandler
先看一下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()); }
擴展 ActionFilterAttribute
註意!註意!註意!
原博文中是擴展的 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); } }
這裡為了方便寫了個ResponseModel,代碼如下:
/// <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; }
簽名驗證的過程如下:
- 獲取到報文Header中的 nonce、timestamp、signature、token信息
- 將token、timestamp、nonce 三者合併數組中,然後進行順序排序(排序為了保證後續三個字元串拼接後一致)
- 將數組拼接成字元串,然後進行sha256 哈希運算(這裡隨便什麼運算都行,主要為了防止超長加密麻煩)
- 將上一步的哈希結果與[signature] RSA解密結果進行比對,一致則簽名驗證通過,否則則簽名不一致,請求為偽造
然後,現在需要啟用剛添加的方法過濾器,因為是繼承與屬性,可以全局啟用,或者單個Controller中啟用、或者為某個Action啟用。全局啟用代碼如下:
下的WebApiConfig類添加如下代碼:
config.Filters.Add(new ApiVerifyFilter());
OK,全部完成,最後附上兩個前後的效果對比!
參考博文:
Asp.Net WebAPI中Filter過濾器的使用以及執行順序
寫博文太累了,回家吃螃蟹補補~