背景介紹
最近使用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#endregionusing 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,全部完成,最后附上兩個前后的效果對比!

參考博文:
WebApi安全性 使用TOKEN+簽名驗證
WebAPi介面安全之公鑰私鑰加密
使用OAuth打造webapi認證服務供自己的客戶端使用
Asp.Net WebAPI中Filter過濾器的使用以及執行順序
微信 公眾號開發檔案
寫博文太累了,回家吃螃蟹補補~
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/8137.html
標籤:ASP.NET
上一篇:百度編輯器上傳視頻報Http請求錯誤,.net實作方式
下一篇:正則運算式初探
