使用ActionFilterAttribute 記錄 WebApi Action 請求和返回結果記錄
轉載:https://www.cnblogs.com/hnsongbiao/p/7039666.html
在asp.net mvc 中 webapi 和 mvc 處理訊息是兩個不同的管道,Asp.net mvc 和 webapi 為我們提供的ActionFilterAttribute 攔截器,通過 重寫OnActionExecutingAsync,來 攔截action的請求訊息,當執行OnActionExecutingAsync完成以後才真正進入請求的action中,action執行完後又把控制權給了OnActionExecutedAsync ,這個管道機制可以使我們用它來輕鬆實現 許可權認證、日誌記錄 ,跨域以及很多需要對全域性或者部分請求做手腳的的功能。
大概的流程如下
通過ActionFilterAttribute ,就能攔截action 處理的所有內容,包括請求提交的引數以及返回值。由於asp.net MVC 與webapi 是兩個完全獨立的管道:
- MVC由System.Web.Mvc.ActionFilterAttribute 來做action請求的攔截。
- webapi 由System.Web.Http.Filters.ActionFilterAttribute 來處理。
因此攔截action請求是完全不相干的兩個通道,於此同時,當我們需要註冊全域性的ActionFilterAttribute 這兩個也是分開註冊的:
MVC 直接在System.Web.Mvc.GlobalFilterCollection 這個全域性管道里面註冊ActionFilter ,位置在App_Start目錄>FilterConfig 類>RegisterGlobalFilters 方法 使用引數filters ,filters.Add(new YourMvcAttribute())新增你的mvcActionFilterAttribute 。
wepi API 在System.Web.Http.Filters 中註冊, 在專案的App_Start 目錄>WebApiConfig類中>Register 方法中加入使用 config引數,config.Filters.Add(new YourWebApiAttribute());新增你的 webapiActionFilterAttribute
這樣就可以註冊你的ActionFilterAttribute 成為全域性的Filter,系統中請求經過Action 之前或之後 都會被你的ActionFilter 攔下來做處理然後在轉交下去。
好了道理已經講完了,現在開始我自己要實現的 日誌記錄功能,
需求是記錄所有訪問webapi action的(請求地址、內容、訪問使用者、提交的引數、返回的結果、以及一些客戶端的資訊)
由於MVC 框架 提倡契約程式設計,在你自定義的Attribute 時,需要遵守契約規範, 【YourFilterName】+Attribute ,所以我的filter名字為OperateTrackAttribute
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web; using System.Web.Http.Controllers; using System.Web.Http.Filters; using WebApiTrackLog.Models; namespace WebApiTrackLog.WebApiAttributes { public class OperateTrackAttribute : ActionFilterAttribute { /// <summary> /// 自定義引數 /// </summary> public string msg { get; set; } public OperateTrackAttribute() { } /// <summary> /// 初始化時填入類的說明 /// </summary> /// <param name="message"></param> public OperateTrackAttribute(string message) { msg = message; } private static readonly string key = "enterTime"; public override Task OnActionExecutingAsync(HttpActionContext actionContext, CancellationToken cancellationToken) { if (SkipLogging(actionContext))//是否該類標記為NoLog { return base.OnActionExecutingAsync(actionContext, cancellationToken); } //記錄進入請求的時間 actionContext.Request.Properties[key] = DateTime.Now.ToBinary(); return base.OnActionExecutingAsync(actionContext, cancellationToken); } /// <summary> /// 在請求執行完後 記錄請求的資料以及返回資料 /// </summary> /// <param name="actionExecutedContext"></param> /// <param name="cancellationToken"></param> /// <returns></returns> public override Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken) { object beginTime = null; if (actionExecutedContext.Request.Properties.TryGetValue(key, out beginTime)) { DateTime time = DateTime.FromBinary(Convert.ToInt64(beginTime)); HttpRequest request = HttpContext.Current.Request; string token = request.Headers["token"]; WepApiActionLog apiActionLog = new WepApiActionLog { Id = Guid.NewGuid(), //獲取action名稱 actionName = actionExecutedContext.ActionContext.ActionDescriptor.ActionName, //獲取Controller 名稱 controllerName = actionExecutedContext.ActionContext.ActionDescriptor.ControllerDescriptor.ControllerName, //獲取action開始執行的時間 enterTime = time, //獲取執行action的耗時 costTime = (DateTime.Now - time).TotalMilliseconds, navigator = request.UserAgent, token = token, //獲取使用者token userId = getUserByToken(token), //獲取訪問的ip ip = request.UserHostAddress, userHostName = request.UserHostName, urlReferrer = request.UrlReferrer != null ? request.UrlReferrer.AbsoluteUri : "", browser = request.Browser.Browser + " - " + request.Browser.Version + " - " + request.Browser.Type, //獲取request提交的引數 paramaters = GetRequestValues(actionExecutedContext), 88 //獲取response響應的結果 executeResult = GetResponseValues(actionExecutedContext), comments = msg, RequestUri = request.Url.AbsoluteUri }; using (TrackLogEntities context = new TrackLogEntities()) { context.WepApiActionLogs.Add(apiActionLog); context.SaveChanges(); } } return base.OnActionExecutedAsync(actionExecutedContext, cancellationToken); } /// <summary> /// 獲取當前登入使用者的id /// </summary> /// <param name="token"></param> /// <returns></returns> public static int getUserByToken(string token) { UserInfo user = null; // TokenManager.getUserByToken(token, out user); return user == null ? 0 : user.user_id; } /// <summary> /// 讀取request 的提交內容 /// </summary> /// <param name="actionExecutedContext"></param> /// <returns></returns> public string GetRequestValues(HttpActionExecutedContext actionExecutedContext) { Stream stream = actionExecutedContext.Request.Content.ReadAsStreamAsync().Result; Encoding encoding = Encoding.UTF8; /* 這個StreamReader不能關閉,也不能dispose, 關了就傻逼了 因為你關掉後,後面的管道 或攔截器就沒辦法讀取了 */ var reader = new StreamReader(stream, encoding); string result = reader.ReadToEnd(); /* 這裡也要注意: stream.Position = 0; 當你讀取完之後必須把stream的位置設為開始 因為request和response讀取完以後Position到最後一個位置,交給下一個方法處理的時候就會讀不到內容了。 */ stream.Position = 0; return result; } /// <summary> /// 讀取action返回的result /// </summary> /// <param name="actionExecutedContext"></param> /// <returns></returns> public string GetResponseValues(HttpActionExecutedContext actionExecutedContext) { Stream stream = actionExecutedContext.Response.Content.ReadAsStreamAsync().Result; Encoding encoding = Encoding.UTF8; /* 這個StreamReader不能關閉,也不能dispose, 關了就傻逼了 因為你關掉後,後面的管道 或攔截器就沒辦法讀取了 */ var reader = new StreamReader(stream, encoding); string result = reader.ReadToEnd(); /* 這裡也要注意: stream.Position = 0; 當你讀取完之後必須把stream的位置設為開始 因為request和response讀取完以後Position到最後一個位置,交給下一個方法處理的時候就會讀不到內容了。 */ stream.Position = 0; return result; } /// <summary> /// 判斷類和方法頭上的特性是否要進行Action攔截 /// </summary> /// <param name="actionContext"></param> /// <returns></returns> private static bool SkipLogging(HttpActionContext actionContext) { return actionContext.ActionDescriptor.GetCustomAttributes<NoLogAttribute>().Any() || actionContext.ActionDescriptor.ControllerDescriptor.GetCustomAttributes<NoLogAttribute>().Any(); } } }
如果將webapi 的OperateTrackAttribute 註冊為webapi全域性的 ActionFilter 那麼我們如果有不想過濾的action 時,可以通過 檢查方法或類頂部特性 來對那些不需要接受攔擊的 Controller 和action 頂部新增一個這樣的特性來區分開,並通過在filter中檢查是被攔截的action或controller 否包含此特性標記,不包含時攔截。
下面是這個類的寫法,一個空的類 繼承Attribute,並在類頂部寫出該Attribute 使用的範圍
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = true)]
public class NoLogAttribute : Attribute
{
}
這樣我們的攔截就更靈活了,無論是添加了整個個Controller 的攔截還是全域性攔截,只需要在不攔截的 controller 或action頭部加上[NoLog]
例如 /// <summary> /// 記錄該類中的Action內容 /// </summary> [OperateTrack] public class TestApiLogController : ApiController { [HttpPost] public object Login(UserInfo user) { var result = new { data = user, status = true }; return result; } /// <summary> /// 該類不參與記錄 /// </summary> /// <param name="name"></param> /// <returns></returns> [NoLog] public string DontLogMe(string name) { return name; } } 或者 /// <summary> /// 該Controller 下的所有action 都不會被全域性的OperateTrack Filter 攔截 /// </summary> [NoLog] public class UserManagerController : ApiController { public List<string> GetUsers() { return new List<string>() { "tomers","jack"}; } public string GiveUserSomeMoney(int money) { return money+""; } }
我們來測試一下: 提交到/api/TestApiLog/Login 整個action 被標記為攔截
再看看記錄的結果,結果已經記錄了
這樣整個記錄使用者訪問記錄的攔截器就到此為止了。