聊一聊Asp.net過濾器Filter那一些事
最近在整理優化.net程式碼時,發現幾個很不友好的處理現象:登入判斷、許可權認證、日誌記錄、異常處理等通用操作,在專案中的action中到處都是。在程式碼優化上,這一點是很重要著力點。這時.net中的過濾器、攔截器(Filter)就派上用場了。現在根據這幾天的實際工作,對其做了一個簡單的梳理,分享出來,以供大家參考交流,如有寫的不妥之處,多多指出,多多交流。
概述:
.net中的Filter中主要包括以下4大類:Authorize(授權),ActionFilter(自定義),HandleError(錯誤處理)。
過濾器 |
類名 |
實現介面 |
描述 |
授權 |
AuthorizeAttribute |
IAuthorizationFilter |
此型別(或過濾器)用於限制進入控制器或控制器的某個行為方法,比如:登入、許可權、訪問控制等等 |
異常 |
HandleErrorAttribute |
IExceptionFilter |
用於指定一個行為,這個被指定的行為處理某個行為方法或某個控制器裡面丟擲的異常,比如:全域性異常統一處理。 |
自定義 |
ActionFilterAttribute |
IActionFilter和IResultFilter |
用於進入行為之前或之後的處理或返回結果的之前或之後的處理,比如:使用者請求日誌詳情日誌記錄 |
AuthorizeAttribute:認證授權
認證授權主要是對所有action的訪問第一入口認證,對使用者的訪問做第一道監管過濾攔截閘口。
實現方式:需要自定義一個類,繼承AuthorizeAttribute並重寫OnAuthorization,在OnAuthorization中能夠獲取到使用者請求的所有Request資訊,其實我們做的所有認證攔截操作,其所有資料支撐都是來自Request中。
具體驗證流程設計:
IP白名單:這個主要針對的是API做IP限制,只有指定IP才可訪問,非指定IP直接返回
請求頻率控制:這個主要是控制使用者的訪問頻率,主要是針對API做,超出請求頻率直接返回。
登入認證:登入認證一般我們採用的是通過在請求的header中傳遞token的方式來進行驗證,這樣即使用與一般的MVC登入認證,也使用與API介面的Auth認證,並且也不依賴於使用者前端js設定等。
授權認證:授權認證就簡單了,主要是驗證該使用者是否具有該許可權,如果不具有,直接做下相應的返回處理。
MVC和API異同:
名稱空間:MVC:System.Web.Http.Filters;API:System.Web.Mvc
注入方式:在注入方式上,主要包括:全域性->控制器Controller->行為Action
全域性註冊:針對所有系統的所有Aciton都使用
Controller:只針對該Controller下的Action起作用
Action:只針對該Action起作用
其中全域性註冊,針對MVC和API還有一些差異:
MVC在 FilterConfig.cs中注入
filters.Add(new XYHMVCAuthorizeAttribute());
API 在 WebApiConfig.cs 中注入
config.Filters.Add(new XYHAPIAuthorizeAttribute());
注意事項:在實際使用中,針對認證授權,我們一般都是新增全域性認證,但是,有的action又不需要做認證,比如本來的登入Action等等,那麼該如何排除呢?其實也很簡單,我們只需要在自定定義一個Attribute整合Attribute,或者系統的AllowAnonymousAttribute,在不需要驗證的action中只需要註冊上對於的Attribute,並在驗證前做一個過濾即可,比如:
// 有 AllowAnonymous 屬性的介面直接開綠燈
if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any())
{
return;
}
API AuthFilterAttribute例項程式碼
/// <summary> /// 授權認證過濾器 /// </summary> public class XYHAPIAuthFilterAttribute : AuthorizationFilterAttribute { /// <summary> /// 認證授權驗證 /// </summary> /// <param name="actionContext">請求上下文</param> public override void OnAuthorization(HttpActionContext actionContext) { // 有 AllowAnonymous 屬性的介面直接開綠燈 if (actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>().Any()) { return; } // 在請求前做一層攔截,主要驗證token的有效性和驗籤 HttpRequest httpRequest = HttpContext.Current.Request; // 獲取apikey var apikey = httpRequest.QueryString["apikey"]; // 首先做IP白名單校驗 MBaseResult<string> result = new AuthCheckService().CheckIpWhitelist(FilterAttributeHelp.GetIPAddress(actionContext.Request), apikey); // 檢驗時間搓 string timestamp = httpRequest.QueryString["Timestamp"]; if (result.Code == MResultCodeEnum.successCode) { // 檢驗時間搓 result = new AuthCheckService().CheckTimestamp(timestamp); } if (result.Code == MResultCodeEnum.successCode) { // 做請求頻率驗證 string acitonName = actionContext.ActionDescriptor.ActionName; string controllerName = actionContext.ActionDescriptor.ControllerDescriptor.ControllerName; result = new AuthCheckService().CheckRequestFrequency(apikey, $"api/{controllerName.ToLower()}/{acitonName.ToLower()}"); } if (result.Code == MResultCodeEnum.successCode) { // 簽名校驗 // 獲取全部的請求引數 Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters(); result = new AuthCheckService().SignCheck(queryParameters, apikey); if (result.Code == MResultCodeEnum.successCode) { // 如果有NoChekokenFilterAttribute 標籤 那麼直接不做token認證 if (actionContext.ActionDescriptor.GetCustomAttributes<XYHAPINoChekokenFilterAttribute>().Any()) { return; } // 校驗token的有效性 // 獲取一個 token string token = httpRequest.Headers.GetValues("Token") == null ? string.Empty : httpRequest.Headers.GetValues("Token")[0]; result = new AuthCheckService().CheckToken(token, apikey, httpRequest.FilePath); } } // 輸出 if (result.Code != MResultCodeEnum.successCode) { // 一定要例項化一個response,是否最終還是會執行action中的程式碼 actionContext.Response = new HttpResponseMessage(HttpStatusCode.OK); //需要自己指定輸出內容和型別 HttpContext.Current.Response.ContentType = "text/html;charset=utf-8"; HttpContext.Current.Response.Write(JsonConvert.SerializeObject(result)); HttpContext.Current.Response.End(); // 此處結束響應,就不會走路由系統 } } }
MVC AuthFilterAttribute例項程式碼
/// <summary> /// MVC自定義授權 /// 認證授權有兩個重寫方法 /// 具體的認證邏輯實現:AuthorizeCore 這個裡面寫具體的認證邏輯,認證成功返回true,反之返回false /// 認證失敗處理邏輯:HandleUnauthorizedRequest 前一步返回 false時,就會執行到該方法中 /// 但是,我平時在應用過程中,一般都是在AuthorizeCore根據不同的認證結果,直接做認證後的邏輯處理 /// </summary> public class XYHMVCAuthorizeAttribute : AuthorizeAttribute { /// <summary> /// 認證邏輯 /// </summary> /// <param name="filterContext">過濾器上下文</param> public override void OnAuthorization(AuthorizationContext filterContext) { // 此處主要寫認證授權的相關驗證邏輯 // 該部分的驗證一般包括兩個部分 // 登入許可權校驗 // --我們的一般處理方式是,通過header中傳遞一個token來進行邏輯驗證 // --當然不同的系統在設計上也不盡相同,有的也會採用session等方式來驗證 // --所以最終還是根據其專案本身的實際情況來進行對應的邏輯操作 // 具體的頁面許可權校驗 // --該部分的驗證是具體的到頁面許可權驗證 // --我看有得小夥伴沒有做到這一個程度,直接將這一步放在前端js來驗證,這樣不是很安全,但是可以攔住小白使用者 // --當然有的系統根本就沒有做許可權控制,那就更不需要這一個邏輯了。 // --所以最終還是根據其專案本身的實際情況來進行對應的邏輯操作 // 現在用一個粗暴的方式來簡單模擬實現過,用系統當前時間段秒廚藝3,取餘數 // 當餘數為0:認證授權通過 // 1:代表為登入,調整至登入頁面 // 2:代表無訪問許可權,調整至無許可權提示頁面 // 當然,在這也還可以做一些IP白名單,IP黑名單驗證 請求頻率驗證等等 // 說到這而,還有一點需要注意,如果我們選擇的是全域性註冊該過濾器,那麼如果有的頁面根本不需要許可權認證,比如登入頁面,那麼我們可以給不需要許可權的認證的控制器或者action新增一個特殊的註解 AllowAnonymous ,來排除 // 獲取Request的幾個關鍵資訊 HttpRequest httpRequest = HttpContext.Current.Request; string acitonName = filterContext.ActionDescriptor.ActionName; string controllerName = filterContext.ActionDescriptor.ControllerDescriptor.ControllerName; // 注意:如果認證不通過,需要設定filterContext.Result的值,否則還是會執行action中的邏輯 filterContext.Result = null; int thisSecond = System.DateTime.Now.Second; switch (thisSecond % 3) { case 0: // 認證授權通過 break; case 1: // 代表為登入,調整至登入頁面 // 只有設定了Result才會終結操作 filterContext.Result = new RedirectResult("/html/Login.html"); break; case 2: // 代表無訪問許可權,調整至無許可權提示頁面 filterContext.Result = new RedirectResult("/html/NoAuth.html"); break; } } }
ActionFilter:自定義過濾器
自定義過濾器,主要是監控action請求前後,處理結果返回前後的事件。其中API只有請求前後的兩個方法。
重新方法 |
方法功能描述 |
使用於 |
OnActionExecuting |
一個請求在進入到aciton邏輯前執行 |
MVC、API |
OnActionExecuted |
一個請求aciton邏輯執行後執行 |
MVC、API |
OnResultExecuting |
對應的view檢視渲染前執行 |
MVC |
OnResultExecuted |
對應的view檢視渲染後執行 |
MVC |
在這幾個方法中,我們一般主要用來記錄互動日誌,記錄每一個步驟的耗時情況,以便後續系統優化使用。具體的使用,根據自身的業務場景使用。
其中MVC和API的異同點,和上面說的認證授權的異同類似,不在詳細說明。
下面的一個例項程式碼:
API定義過濾器例項DEMO程式碼
/// <summary> /// Action過濾器 /// </summary> public class XYHAPICustomActionFilterAttribute : ActionFilterAttribute { /// <summary> /// Action執行開始 /// </summary> /// <param name="actionContext"></param> public override void OnActionExecuting(HttpActionContext actionContext) { } /// <summary> /// action執行以後 /// </summary> /// <param name="actionContext"></param> public override void OnActionExecuted(HttpActionExecutedContext actionContext) { try { // 構建一個日誌資料模型 MApiRequestLogs apiRequestLogsM = new MApiRequestLogs(); // API名稱 apiRequestLogsM.API = actionContext.Request.RequestUri.AbsolutePath; // apiKey apiRequestLogsM.API_KEY = HttpContext.Current.Request.QueryString["ApiKey"]; // IP地址 apiRequestLogsM.IP = FilterAttributeHelp.GetIPAddress(actionContext.Request); // 獲取token string token = HttpContext.Current.Request.Headers.GetValues("Token") == null ? string.Empty : HttpContext.Current.Request.Headers.GetValues("Token")[0]; apiRequestLogsM.TOKEN = token; // URL apiRequestLogsM.URL = actionContext.Request.RequestUri.AbsoluteUri; // 返回資訊 var objectContent = actionContext.Response.Content as ObjectContent; var returnValue = objectContent.Value; apiRequestLogsM.RESPONSE_INFOR = returnValue.ToString(); // 由於資料庫中最大隻能儲存4000字串,所以對返回值做一個擷取 if (!string.IsNullOrEmpty(apiRequestLogsM.RESPONSE_INFOR) && apiRequestLogsM.RESPONSE_INFOR.Length > 4000) { apiRequestLogsM.RESPONSE_INFOR = apiRequestLogsM.RESPONSE_INFOR.Substring(0, 2000); } // 請求引數 apiRequestLogsM.REQUEST_INFOR = actionContext.Request.RequestUri.Query; // 定義一個非同步委託 ,非同步記錄日誌 // Func<MApiRequestLogs, string> action = AddApiRequestLogs;//宣告一個委託 // IAsyncResult ret = action.BeginInvoke(apiRequestLogsM, null, null); } catch (Exception ex) { } } }
HandleError:錯誤處理
異常處理對於我們來說很常用,很好的利用異常處理,可以很好的避免全篇的try/catch。異常處理箱單很簡單,值需要自定義整合:ExceptionFilterAttribute,並自定義實現:OnException方法即可。
在OnException我們可以根據自身需要,做一些相應的邏輯處理,比如記錄異常日誌,便於後續問題分析跟進。
OnException還有一個很重要的處理,那就是對異常結果的統一包裝,返回一個很友好的結果給使用者,避免把一些不必要的資訊返回給使用者。比如:針對MVC,那麼跟進不同異常,統一調整至友好的提示頁面等等;針對API,那麼我們可以一個統一的返回幾個封裝,便於使用者統一處理結果。
MVC 的異常處理例項程式碼:
/// <summary> /// MVC自定義異常處理機制 /// 說道異常處理,其實我們腦海中的第一反應,也該是try/cache操作 /// 但是在實際開發中,很有可能地址錯誤根本就進入不到try中,又或者沒有被try處理到異常 /// 該類就發揮了作用,能夠很好的未經捕獲的異常,並做相應的邏輯處理 /// 自定義異常機制,主要整合HandleErrorAttribute 重寫其OnException方法 /// </summary> public class XYHMVCHandleError : HandleErrorAttribute { /// <summary> /// 處理異常 /// </summary> /// <param name="filterContext">異常上下文</param> public override void OnException(ExceptionContext filterContext) { // 我們在平時的專案中,異常處理一般有兩個作用 // 1:記錄異常的詳細日誌,便於事後分析日誌 // 2:對異常的統一友好處理,比如根據異常型別重定向到友好提示頁面 // 在這裡面既能獲取到未經處理的異常資訊,也能獲取到請求資訊 // 在此可以根據實際專案需要做相應的邏輯處理 // 下面簡單的列舉了幾個關鍵資訊獲取方式 // 控制器名稱 注意,這樣獲取出來的是一個檔案的全路徑 string contropath = filterContext.Controller.ToString(); // 訪問目錄的相對路徑 string filePath = filterContext.HttpContext.Request.FilePath; // url完整地址 string url = (filterContext.HttpContext.Request.Url.AbsoluteUri).ExUrlDeCode(); // 請求方式 post get string httpMethod = filterContext.HttpContext.Request.HttpMethod; // 請求IP地址 string ip = filterContext.HttpContext.Request.GetIPAddress(); // 獲取全部的請求引數 HttpRequest httpRequest = HttpContext.Current.Request; Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters(); // 獲取異常物件 Exception ex = filterContext.Exception; // 異常描述資訊 string exMessage = ex.Message; // 異常堆疊資訊 string stackTrace = ex.StackTrace; // 根據實際情況記錄日誌(文字日誌、資料庫日誌,建議具體步驟採用非同步方式來完成) filterContext.ExceptionHandled = true; // 模擬根據不同的做對應的邏輯處理 int statusCode = filterContext.HttpContext.Response.StatusCode; if (statusCode>=400 && statusCode<500) { filterContext.Result = new RedirectResult("/html/404.html"); } else { filterContext.Result = new RedirectResult("/html/500.html"); } } }
API 的異常處理例項程式碼:
/// <summary> /// API自定義異常處理機制 /// 說道異常處理,其實我們腦海中的第一反應,也該是try/cache操作 /// 但是在實際開發中,很有可能地址錯誤根本就進入不到try中,又或者沒有被try處理到異常 /// 該類就發揮了作用,能夠很好的未經捕獲的異常,並做相應的邏輯處理 /// 自定義異常機制,主要整合ExceptionFilterAttribute 重寫其OnException方法 /// </summary> public class XYHAPIHandleError : ExceptionFilterAttribute { /// <summary> /// 處理異常 /// </summary> /// <param name="actionExecutedContext">異常上下文</param> public override void OnException(HttpActionExecutedContext actionExecutedContext) { // 我們在平時的專案中,異常處理一般有兩個作用 // 1:記錄異常的詳細日誌,便於事後分析日誌 // 2:對異常的統一友好處理,比如根據異常型別重定向到友好提示頁面 // 在這裡面既能獲取到未經處理的異常資訊,也能獲取到請求資訊 // 在此可以根據實際專案需要做相應的邏輯處理 // 下面簡單的列舉了幾個關鍵資訊獲取方式 // action名稱 string actionName = actionExecutedContext.ActionContext.ActionDescriptor.ActionName; // 控制器名稱 string controllerName =actionExecutedContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName; // url完整地址 string url = (actionExecutedContext.Request.RequestUri.AbsoluteUri).ExUrlDeCode(); // 請求方式 post get string httpMethod = actionExecutedContext.Request.Method.Method; // 請求IP地址 string ip = actionExecutedContext.Request.GetIPAddress(); // 獲取全部的請求引數 HttpRequest httpRequest = HttpContext.Current.Request; Dictionary<string, string> queryParameters = httpRequest.GetAllQueryParameters(); // 獲取異常物件 Exception ex = actionExecutedContext.Exception; // 異常描述資訊 string exMessage = ex.Message; // 異常堆疊資訊 string stackTrace = ex.StackTrace; // 根據實際情況記錄日誌(文字日誌、資料庫日誌,建議具體步驟採用非同步方式來完成) // 自己的記錄日誌落地邏輯略 ...... // 構建統一的內部異常處理機制,相當於對異常做一層統一包裝暴露 MBaseResult<string> result = new MBaseResult<string>() { Code = MResultCodeEnum.systemErrorCode, Message = MResultCodeEnum.systemError }; actionExecutedContext.Response = new HttpResponseMessage(HttpStatusCode.OK); //需要自己指定輸出內容和型別 HttpContext.Current.Response.ContentType = "text/html;charset=utf-8"; HttpContext.Current.Response.Write(JsonConvert.SerializeObject(result)); HttpContext.Current.Response.End(); // 此處結束響應,就不會走路由系統 } }
總結
.net過濾器,我個人的一句話理解就是:對action的各個階段進行統一的監控處理等操作。.net過濾器中,其中每一個種過濾器的執行先後順序為:Authorize(授權)-->ActionFilter(自定義)-->HandleError(錯誤處理)
好了,就先聊到這而,如果什麼地方說的不對之處,多多指點和多多包涵。我自己寫了一個練習DEMO,裡面會有每一種情況的處理說明。有興趣的可以取下載下來看一看,謝謝。
DEMO在GitHub地址為:https://github.com/xuyuanhong0902/XYH.FilterTest.git
END
為了更高的交流,歡迎大家關注我的公眾號,掃描下面二維碼即可關注,謝謝:
認證授權