Asp.Net MVC-4-过滤器1:认证与授权
基础
过滤器体现了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)有效么,要如何处理呢。