基礎
過濾器體現了MVC框架中的Aop思想,雖然這種實現並不完美但在實際的開發過程中一般也足以滿足需求了。
過濾器分類
依據上篇分析的執行時機的不同可以把過濾器按照實現不同的接口分為下面五類:
IAuthenticationFilter 認證和所有IActionFilter執行后(OnAuthentication、OnAuthenticationChallenge)
IAuthorizationFilter 授權(OnAuthorization)
IActionFilter Action執行前后的操作(OnActionExecuting、OnActionExecuted)
IResultFilter Result的執行前后的操作(OnResultExecuting、OnResultExecuted)
IExceptionFilter 處理異常(OnException)
框架已經提供的實現主要有以下幾種
AuthorizeAttribute(實現IAuthorizationFilter)、ChildActionOnlyAttribute(實現IAuthorizationFilter)、ActionFilterAttribute(實現IActionFilter和IResultFilter)、AsyncTimeoutAttribute(繼承ActionFilterAttribute) 、ContentTypeAttribute(繼承ActionFilterAttribute)、CopyAsyncParametersAttribute(繼承ActionFilterAttribute)、WebApiEnabledAttribute(繼承ActionFilterAttribute)、ResetThreadAbortAttribute(繼承ActionFilterAttribute)、HandleErrorAttribute(實現IExceptionFilter)、OutputCacheAttribute(繼承ActionFilterAttribute並實現IExceptionFilter)、Controller(實現所有過濾器接口),對於各種實現的用途大家可以查看源碼,這里只講一下Controller,這是所有我們定義的Controller的基類,因此通過重載Controller的各種過濾器接口的實現就可以實現過濾器的效果而不必使用特性或在GlobalFilters中注冊,這是一種非常方便的做法但是缺少一定的靈活性。
過濾器的注冊和獲取有三種方式:特性、Controller、Global。
來看過濾器是如何獲取的,前面分析的ControllerActionInvoker的InvokeAction方法中獲取過濾器的方法是GetFilters,它實質是調用一個委托
protected virtual FilterInfo GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) { return new FilterInfo(_getFiltersThunk(controllerContext, actionDescriptor)); }
private Func<ControllerContext, ActionDescriptor, IEnumerable<Filter>> _getFiltersThunk = FilterProviders.Providers.GetFilters;
該委托調用FilterProviders類的一個靜態FilterProviderCollection類型變量Providers的方法GetFilters,我們可以發現FilterProviderCollection類型是一個FilterProvider的集合,獲取Filters要通過每一個FilterProvider調用其GetFilters方法。
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) { …… IFilterProvider[] providers = CombinedItems; List<Filter> filters = new List<Filter>(); for (int i = 0; i < providers.Length; i++) { IFilterProvider provider = providers[i]; foreach (Filter filter in provider.GetFilters(controllerContext, actionDescriptor)) { filters.Add(filter); } } filters.Sort(_filterComparer); if (filters.Count > 1) { RemoveDuplicates(filters); } return filters; }
那這個集合里面有哪些FilterProvider呢,從FilterProviders的靜態構造函數中可以找到答案
static FilterProviders() { Providers = new FilterProviderCollection(); Providers.Add(GlobalFilters.Filters); Providers.Add(new FilterAttributeFilterProvider()); Providers.Add(new ControllerInstanceFilterProvider()); }
第一個是全局的GlobalFilterCollection,它既是一個Filter的集合又實現了IFilterProvider,這是我們設置全局過濾器的地方,查看這里過濾器是如何添加的:
private void AddInternal(object filter, int? order) { ValidateFilterInstance(filter); _filters.Add(new Filter(filter, FilterScope.Global, order)); } private static void ValidateFilterInstance(object instance) { if (instance != null && !( instance is IActionFilter || instance is IAuthorizationFilter || instance is IExceptionFilter || instance is IResultFilter || instance is IAuthenticationFilter)) { throw Error.InvalidOperation(MvcResources.GlobalFilterCollection_UnsupportedFilterInstance, typeof(IAuthorizationFilter).FullName, typeof(IActionFilter).FullName, typeof(IResultFilter).FullName, typeof(IExceptionFilter).FullName, typeof(IAuthenticationFilter).FullName); } }
可以看到首先驗證過濾器是否實現了上面所講的五個接口中的一個,然后再依據此對象創建Filter對象(Filter與真正的過濾器是不同的,Filter的Instance屬性可以看做保存了真正的過濾器,另外的兩個屬性Order和Scope主要用來排序用,這點后面再講)並加到集合中。Filter的構造函數如下:
public Filter(object instance, FilterScope scope, int? order) { if (instance == null) { throw new ArgumentNullException("instance"); } if (order == null) { IMvcFilter mvcFilter = instance as IMvcFilter; if (mvcFilter != null) { order = mvcFilter.Order; } } Instance = instance; Order = order ?? DefaultOrder; Scope = scope; }
第二個FilterProvider是FilterAttributeFilterProvider,這是通過反射獲取特性從而獲取過濾器的地方。
public virtual IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) { if (controllerContext.Controller != null) { foreach (FilterAttribute attr in GetControllerAttributes(controllerContext, actionDescriptor)) { yield return new Filter(attr, FilterScope.Controller, order: null); } foreach (FilterAttribute attr in GetActionAttributes(controllerContext, actionDescriptor)) { yield return new Filter(attr, FilterScope.Action, order: null); } } }
第三個ControllerInstanceFilterProvider是通過Controller創建過濾器的,或者是Controller本身就是一個過濾器(正如前面所言Controller實現了所有類型的過濾器接口)
public IEnumerable<Filter> GetFilters(ControllerContext controllerContext, ActionDescriptor actionDescriptor) { if (controllerContext.Controller != null) { yield return new Filter(controllerContext.Controller, FilterScope.First, Int32.MinValue); } }
最后我們來分析下Filter類型本身,下面來看看Filter的屬性Scope和Order,這兩者都是用來確定Filter的執行順序的,我們知道在獲取Filter后而在調用之前會調用filters.Sort(_filterComparer)進行排序,_filterComparer是一個FilterComparer類型的比較器,定義如下。
private class FilterComparer : IComparer<Filter> { public int Compare(Filter x, Filter y) { if (x == null && y == null) { return 0; } if (x == null) { return -1; } if (y == null) { return 1; } if (x.Order < y.Order) { return -1; } if (x.Order > y.Order) { return 1; } if (x.Scope < y.Scope) { return -1; } if (x.Scope > y.Scope) { return 1; } return 0; } }
代碼邏輯很清晰:根據Order然后根據Scope排序。Order是一個整形值,通過Filter的構造函數我們可知我們可以在IMvcFilter(FilterAttribute和Controller都實現此接口)中設置此Order值,否則Order會通過構造函數來設置(如果是null則設置為默認的-1)
而FilterScope是一個枚舉類型,三種不同的FilterProvider會設置不同的FilterScope。
public enum FilterScope { First = 0, Global = 10, Controller = 20, Action = 30, Last = 100, }
注意ActionFilter在調用時會先Reverse,使得最優先Filter其實是最靠近Action的(OnActionExecuting和OnActionExecuted調用順序相反)。至於ActionFilter之外的其它類型執行順序是如何確定的通過代碼很容易找出答案。
認證
自MVC4以后,認證和授權就分開來了(符合單一職責原則),前面的篇章分析Action的執行時提到認證是最先執行的,然后是授權。
認證過濾器必須實現接口IAuthenticationFilter,同時如果要作為一種過濾器特性來使用的話必須繼承FilterAttribute。
先來看一個基本的認證實現,這里使用了很普遍的session驗證。
public class CustomAuthenticationAttribute: FilterAttribute,IAuthenticationFilter { public void OnAuthentication(AuthenticationContext filterContext) { var session = filterContext.RequestContext.HttpContext.Session; if (session != null && session["user"]!=null && session["roles"]!=null) { string name = session["user"].ToString(); string[] roles = session["roles"] as string[]; filterContext.Principal = new GenericPrincipal(new GenericIdentity(name), roles); } else { filterContext.Result = new HttpUnauthorizedResult("no authentication"); } } public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext) { filterContext.Result = new SessionChallengeResult() { currentResult = filterContext.Result}; } } class SessionChallengeResult : ActionResult { public ActionResult currentResult { set; get; } public override void ExecuteResult(ControllerContext context) { currentResult.ExecuteResult(context); var rsponse = context.HttpContext.Response; if (rsponse.StatusCode == (int)HttpStatusCode.Unauthorized) { rsponse.Redirect(string.Format("~/{0}/{1}","Account", "Login")); rsponse.End(); } } }
代碼實現很簡單,只通過獲取session中的user和roles來設置Principal(采用基礎的GenericPrincipal和GenericIdentity類型,也可以嘗試其它的或自定義實現IPrincipal和IIdentity接口的類型),但是如果session中沒有這些信息則設置HttpUnauthorizedResult,這會終結Action的執行直接轉到OnAuthenticationChallenge。在OnAuthenticationChallenge中我們通過自定義的SessionChallengeResult類來實現如果是未授權(HttpUnauthorizedResult)的Result執行時跳轉到我們的登錄頁面。
登錄頁面的實現:
首先實現Controller,這里只實現了基本的功能用於驗證,如果要實現自己的認證邏輯可以在Validate中去實現。至於登錄頁面的實現不是重點這里不再貼出來,只是必須有一個提交到Login的form,並且至少有name和password兩個輸入。另外這里用了PRG方式,由於不是本文的重點也就不多加說明了。
public class AccountController : Controller { [HttpGet] public ActionResult Login() { ModelStateDictionary redirectModelState = TempData["tmp_model_state"] as ModelStateDictionary; if (redirectModelState != null) { ModelState.Merge(redirectModelState); } return View(); } public ActionResult Login(string name, string password) { if (ModelState.IsValid) { string[] roles = null; if (Validate(name, password, out roles)) { Session["user"] = name; Session["roles"] = roles; return Redirect("~/Home/Index"); } else { ModelState.AddModelError("loginerror", "用戶名或密碼不正確"); TempData["tmp_model_state"] = ModelState; return Redirect("Login"); } } else { TempData["tmp_model_state"] = ModelState; return Redirect("Login"); } } private bool Validate(string user, string password, out string[] roles) { roles = new string[] {"guest"}; return true; } }
注冊特性
我們采用最簡單的注冊,通過在HomeController上添加屬性[CustomAuthentication]
驗證
再次運行程序,發現已經不能直接進入主頁了,而是來到了登錄頁面,輸入用戶名密碼才可以進入到主頁。同時可以調試程序看程序流程是否如你所料。
授權
先來看一個基本的授權實現,這里我們繼承了AuthorizeAttribute,一般來說這是一種簡單有效的做法。
public class CustomAuthorizeAtrribute: AuthorizeAttribute { public override void OnAuthorization(AuthorizationContext filterContext) { var principal = filterContext.HttpContext.User; if (principal != null) { var identity = principal.Identity; if (identity != null) { bool unAuthorize = string.IsNullOrEmpty(Users) && string.IsNullOrEmpty(Roles); if (unAuthorize) { return; } string[] users = null, roles = null; if (!string.IsNullOrEmpty(Users)) users = Users.Split(','); if (!string.IsNullOrEmpty(Roles)) roles = Roles.Split(','); if (users != null && !string.IsNullOrEmpty(identity.Name)) { foreach (var user in users) { if (string.Compare(identity.Name, user) == 0) { return; } } } if (roles != null) { foreach (var role in roles) { if (principal.IsInRole(role)) { return; } } } } } filterContext.Result = new HttpUnauthorizedResult("no authentication"); } }
處理邏輯是很簡單的驗證用戶名和角色是否匹配,值得注意的是增加了多用戶和多角色的支持(以逗號分隔)。在HomeController的Action上加上授權過濾器並設置不同的roles,分別訪問這些Acion可以驗證授權過濾器的作用(前面實現的登錄設置的role都是guest)
[CustomAuthorize(Roles = "guest,admin")] public ActionResult Index() [CustomAuthorize(Roles = "admin")] public ActionResult About() [CustomAuthorize(Roles = "guest")] public ActionResult Contact()
另外一種實現
通過在Controller或者Action上添加特性來實現過濾器的做法既繁雜又可能導致遺漏,而且需要修改時更是麻煩,下面給大家提供一種基於全局的認證和授權方式作為一種參考
首先添加認證和授權過濾器,下面是認證的實現類
public class GlobalAuthenticationFilter: IAuthenticationFilter { public void OnAuthentication(AuthenticationContext filterContext) { if (filterContext.IsChildAction) { return; } var session = filterContext.RequestContext.HttpContext.Session; if (session != null && session["user"] != null && session["roles"] != null) { string name = session["user"].ToString(); string[] roles = session["roles"] as string[]; filterContext.Principal = new GenericPrincipal(new GenericIdentity(name), roles); } else { return; } } public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext) { filterContext.Result = new SessionChallengeResult() { currentResult = filterContext.Result }; } }
可以看到與前面認證實現類不同的是不再繼承FilterAttribute(因此不能作為特性),對於部分試圖(IsChildAction)的請求不作處理,然后在session中不存在user和roles時不作處理,這是為了我們能夠訪問登錄頁面,而除了登錄頁面之外的訪問控制交由授權來實現。
接下來看授權的實現類
public class GlobalAuthorizeFilter:IAuthorizationFilter { if (filterContext.IsChildAction) { return; } public String Users { get; set; } public String Roles { get; set; } public void OnAuthorization(AuthorizationContext filterContext) { string accountControllerName = "Account"; string loginActionName = "Login"; string controllerName = (string)filterContext.RouteData.Values["controller"]; string actionName = (string)filterContext.RouteData.Values["action"];
if (string.Compare(accountControllerName, controllerName, true) == 0 && string.Compare(loginActionName, actionName, true) == 0) { return; } var principal = filterContext.HttpContext.User; if (principal != null) { var identity = principal.Identity; if (identity != null) { bool unAuthorize = string.IsNullOrEmpty(Users) && string.IsNullOrEmpty(Roles); if (unAuthorize) { return; } string[] users = null, roles = null; if (!string.IsNullOrEmpty(Users)) users = Users.Split(','); if (!string.IsNullOrEmpty(Roles)) roles = Roles.Split(','); if (users != null && !string.IsNullOrEmpty(identity.Name)) { foreach (var user in users) { if (string.Compare(identity.Name, user) == 0) { return; } } } if (roles != null) { foreach (var role in roles) { if (principal.IsInRole(role)) { return; } } } } } filterContext.Result = new HttpUnauthorizedResult("no authentication"); } }
可以看到新的授權類不再繼承AuthorizeAttribute,注意前面幾行代碼的作用就是給登錄頁面放行,而其他頁面如果未登錄則會重定向到登錄頁面。
最后我們在FilterConfig中注冊全局過濾器
public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); filters.Add(new GlobalAuthenticationFilter()); filters.Add(new GlobalAuthorizeFilter() {Roles = "guest"}); }
通過運行查看頁面可以驗證過濾器,這種做法的問題主要在於整個程序的授權只能采用同一種策略,那么如何實現對不同url的不同授權方式呢,大家可以試着實現它(這當然是可以實現的)。另外一個問題是如果有多個授權和認證過濾器的話該如何考慮組合在一起呢,比如我們能否使用GlobalAuthenticationFilter做認證而組合GlobalAuthorizeFilter和CustomAuthorizeAtrribute做授權呢。最后一個問題:授權和認證過濾器對MVC程序中添加的Asp.net頁面(aspx)有效么,要如何處理呢。
