[转载]ASP.NET MVC权限设计思考

转载.NET MVC权限设计思考 – 小城岁月 – 博客园.

在WebForm下我们一般会设计个PageBase继承Page,在OnInit方法中实现对基本权限的验证业务,然后所有的页面在继承 PageBase直接继承这项基本权验证业务。而在.NET MVC下我们如何再实现这个业务呢? 其实无非也是要设计一个ExtController基类来实现这个业务,而这个ExtController基类的权 限验证业务切入点选在哪里合适呢? 这个答案还要从前面的 了解.net MVC的实现原理Controller/Action 章节寻找。(标签属性IActionFilter, IAuthorizationFilter暂且不涉及)

一、寻找合适时机的切入点

简单的回顾一下这个过程,首先Controller中的Action要被执行,那Controller就要被实例化,接着才能根据请求的URL,调用对应 的Action。Controller是被MvcHandler的ProcessRequest或BeginProcessRequest方法的这段代码 创建的。

View Code

  private void ProcessRequestInit(HttpContextBase httpContext, out IController controller, out IControllerFactory factory) {
            // If request validation has already been enabled, make it lazy. This allows attributes like [HttpPost] (which looks
            // at Request.Form) to work correctly without triggering full validation.
            bool? isRequestValidationEnabled = ValidationUtility.IsValidationEnabled(HttpContext.Current);
            if (isRequestValidationEnabled == true) {
                ValidationUtility.EnableDynamicValidation(HttpContext.Current);
            }

            AddVersionHeader(httpContext);
            RemoveOptionalRoutingParameters();

            // Get the controller type
            string controllerName = RequestContext.RouteData.GetRequiredString("controller");

            // Instantiate the controller and call Execute
            factory = ControllerBuilder.GetControllerFactory();
            controller = factory.CreateController(RequestContext, controllerName);
            if (controller == null) {
                throw new InvalidOperationException(
                    String.Format(
                        CultureInfo.CurrentCulture,
                        MvcResources.ControllerBuilder_FactoryReturnedNull,
                        factory.GetType(),
                        controllerName));
            }
        }

相信很多人都通IOC容器重新实现过ControllerBuilder的DefaultControllerFactory

View Code

            ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory(ContainerFactory.GetContainer()));

通过上面代码我们可以知道Controller是在何时被创建的,我们第一次有机会拦截Controller对其做一定处理的时机,就是重新实现 ControllerBuilder的DefaultControllerFactory中的这个方法GetControllerInstance。我们 再继续寻找其它的切入点,当MvcHandler通过ControllerFactory创建了请求对应的Controller的实例后,会继续调用自己 的这个方法

View Code

  protected internal virtual void ProcessRequest(HttpContextBase httpContext) {
            SecurityUtil.ProcessInApplicationTrust(() => {
                IController controller;
                IControllerFactory factory;
                ProcessRequestInit(httpContext, out controller, out factory);

                try {
                    controller.Execute(RequestContext);
                }
                finally {
                    factory.ReleaseController(controller);
                }
            });
        }

执行IController接口的Execute方法,我们来看一下最终是执行的Controller.Execute->ControllerBase.Execute的方法

View Code

  protected virtual void Execute(RequestContext requestContext) {
            if (requestContext == null) {
                throw new ArgumentNullException("requestContext");
            }
            if (requestContext.HttpContext == null) {
                throw new ArgumentException(MvcResources.ControllerBase_CannotExecuteWithNullHttpContext, "requestContext");
            }

            VerifyExecuteCalledOnce();
            Initialize(requestContext);

            using (ScopeStorage.CreateTransientScope()) {
                ExecuteCore();
            }
        }

注意ControllerBase中Execute本身这个方法,以及其中的Initialize方法,ExecuteCore方法均是 Action被执行前的实现基本权限验证业务的切入点。

另外在Controller中ExecuteCore 方法被重载实现

View Code

 protected override void ExecuteCore() {
            // If code in this method needs to be updated, please also check the BeginExecuteCore() and
            // EndExecuteCore() methods of AsyncController to see if that code also must be updated.

            PossiblyLoadTempData();
            try {
                string actionName = RouteData.GetRequiredString("action");
                if (!ActionInvoker.InvokeAction(ControllerContext, actionName)) {
                    HandleUnknownAction(actionName);
                }
            }
            finally {
                PossiblySaveTempData();
            }
        }

其中ActionInvoker.InvokeAction方法是反射执行对应URL请求的Action,在这个方法,我们可以清楚看到当在Action被执行前,又分别先执行了Controller中的这个两个方法

View Code

 protected override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);
        }

        protected override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);
        }

因此Controller的OnActionExecuting,OnAuthorization也可以做为Action 被执行前的权限验证切入点。好了,通过源码找到了这么多的切入点,那我们就分别来实现一下试试。

二、权限验证切入点的尝试

首先设计个继承Controller的基类BaseController,详细设计请参照注释

View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Demo.Mvc.Core
{
    /// <summary>
    /// 登录成功的时候被放入SESSION中
    /// </summary>
    public class UserState
    {
        public string PassCode { set; get; }
        public string UserId { set; get; }
        public string UserName { set; get; }

        //public List<Role> RoleCollection { set; get; }   //角色集
        //public List<Auth> AuthCollection { set; get; }   //权限集
    }

    public class BaseController:System.Web.Mvc.Controller 
    {
        /// <summary>
        /// 用户信息
        /// </summary>
        public UserState UserState { set; get; }

        /// <summary>
        /// 微软设计这个无参的构造的Controller 有利于使用IOC容器提高对象的创建效率
        /// 如果设计了System.Web.Routing.RequestContext参数,由于每次来的RequestContext都不相同
        /// 则Controller 就要不停的动态创建
        /// </summary>
        public BaseController()
        {
            //无参的构造
        }

        /// <summary>
        /// 改造一个构造函数切入点
        /// 这种方式虽然使得切入机会早,并且可以较早的构造中对业务层注入一些用户信息。
        /// 但是缺点就是每次都要动态反射(因为每次来的HttpContext请求都不相同)
        /// </summary>
        /// <param name="requestContext"></param>
        public BaseController(System.Web.Routing.RequestContext requestContext)
        {
            this.OnInit(requestContext);  //这样可以在构造的时候就切入了
        }

        /// <summary>
        /// 比较早的切入点 在ControllerFactory被创建的时候顺便就实现权限验证
        /// </summary>
        /// <param name="requestContext"></param>
        public virtual void OnInit(System.Web.Routing.RequestContext requestContext)
        {
            //这里实现用户信息的相关验证业务
            if (requestContext.HttpContext.Session["UserState"] != null)
            {
                UserState userState = requestContext.HttpContext.Session["UserState"] as UserState;
                string passCode = requestContext.HttpContext.Request.Cookies["UserState"].Value.Trim();

                string controllerName =  requestContext.RouteData.Values["controller"].ToString()+"Controller";
                string actionName = requestContext.RouteData.Values["action"].ToString();

               //判断有没有Action操作权限
               //userState.AuthCollection.Contains(controllerName + "/" + acitonName);
            }
            else
            {
                //非登录用户跳转
                //requestContext.HttpContext.Response.Redirect("/html/complex.html");
            }            
        }

        /// <summary>
        /// 比较晚的切入点 IController在执行Execute之后,Action被执行之前使用的
        /// </summary>
        public virtual void OnInit()
        {
            //这里实现用户信息的相关验证业务
            if (this.HttpContext.Session["UserState"] != null)
            {
                UserState userState = this.HttpContext.Session["UserState"] as UserState;
                string passCode = this.HttpContext.Request.Cookies["UserState"].Value.Trim();

                string controllerName = this.RouteData.Values["controller"].ToString() + "Controller";
                string actionName = this.RouteData.Values["action"].ToString();

                //实现Action操作权限验证业务
                //userState.AuthCollection.Contains(controllerName + "/" + acitonName);
            }
            else
            {
                //非登录用户跳转
                //requestContext.HttpContext.Response.Redirect("/html/complex.html");
            }
        }       
        

        protected override void Execute(System.Web.Routing.RequestContext requestContext)
        {             
            base.Execute(requestContext);
            //this.OnInit();//---------------------------------------------切入点
        }

        protected override void ExecuteCore()
        {            
            base.ExecuteCore();
            //this.OnInit();//---------------------------------------------切入点
        }

        protected override void Initialize(System.Web.Routing.RequestContext requestContext)
        {         
            base.Initialize(requestContext);
            this.OnInit();  //---------------------------------------------切入点 
        }

        //除上述的方式以下方式 
        //我们还可以使用IActionFilter, IAuthorizationFilter标签属性的方式实现权限验证 (这个不在本次讨研究范围内)
        protected override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext)
        {            
            base.OnActionExecuting(filterContext);
            //this.OnInit();//---------------------------------------------切入点
        }

        protected override void OnAuthorization(System.Web.Mvc.AuthorizationContext filterContext)
        {
            base.OnAuthorization(filterContext);
            //this.OnInit();//---------------------------------------------切入点
        }
        
    }
}

里面列出了实现基类的中的多个切入点,其中第一个OnInit 方法的设计 是由于Controller在构造实例时 并没有合适的切入的点,所以通过RequestContext的注入可以使我们可以将切入点提到ControllerFactory中。 第二个OnInit的切入比较晚,都是在IController的 Execute被执行后,对应的Action被执行前。当然第二种情况已经完全满足了我们的业务需要,为什么还要第一种OnInit的设计?我们先来重构 一下ControllerFactory,改变默认的Controller的创建方式。

View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web.Mvc;
using System.Web.Routing;
using Microsoft.Practices.Unity;

namespace Demo.Mvc
{
    public class UnityControllerFactory : DefaultControllerFactory
    {
        private readonly IUnityContainer container;

        public UnityControllerFactory(IUnityContainer container)
        {
            //要做异常处理
            this.container = container;
        }

        protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
        {             
            if (controllerType.IsSubclassOf(typeof(Core.BaseController)))
            {              
                //早期切入点 方式1,使用默认的无构造方式
                //目的必免直接反射Controller,提高效率
                Core.BaseController controller = container.Resolve(controllerType) as Core.BaseController;
                controller.OnInit(requestContext);  //切入点 
                
                //假如这个位置使用构造注入那OnInit就会构造函数执行时就实现相关验证业务包括UserState已经创建
                //这样做导致Controller每次都要被动态反射创建,但好处是可以使用权限切入点更早
                //这样权限业务可以更早的注入业务层中(如UserState传递至业务层)
                //代码省略
                
                return controller;
            }
            else
                return container.Resolve(controllerType) as IController;
        }

        public override IController CreateController(RequestContext requestContext, string controllerName)
        {            
            return base.CreateController(requestContext, controllerName);
        }
      
    }


}

我们再来实现个MenuController ,详细参照注释

View Code

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
using System.ComponentModel;
using Demo.Model;
using Demo.Service.IService;
using Demo.Mvc.ViewObject;

namespace Demo.Mvc.Controller
{    
          
     
    [HandleError]
    public class MenuController:Core.BaseController
    {
        private IMenuService service;

        //如果我们ControllerFactory中使用构造生成Controller,那OnInit在构造的同时也成功获得了UserState
        //并且可以在这个时候将UserState注入到业务层中
        public MenuController(IMenuService service)
        {
             
            this.service = service;
            //this.service.UserState = this.UserState;   
            //这样做的优点就是不用再重载基类的Initialize方法,前提ControllerFactory中使用构造生成Controller,这样可以过早的拿到Session.
        }

        //由于在ControllerFactory无构造生成Controller,就会导OnInit的切入时机比较晚,这个时候UserState还是NULL(即还没有拿到HttpContext中的Session)
        //所以我们只能通过在重载一次基类的方式,才能获得已经创建的UserState信息
        protected override void Initialize(System.Web.Routing.RequestContext requestContext)
        {
            base.Initialize(requestContext);
            //this.service.UserState = this.UserState;    
        }

        public MenuController()
        { }

        public ActionResult ShowContent(object model)
        {
          //service.Update(model,this.UserState)   //用户信息传至业务层 通过业务层方法参数注入
            return View();
        }

        public ActionResult Index(UserVO userVo)
        {
            if (ModelState.IsValid)
            {
                ViewData["Key1"] = "TEST1";
                TempData["Key2"] = "TEST2";
            }
            else
            {
                 
            }
            return View();   //默认封装ViewResult返回   
        }

        public ActionResult DownLoadFile(string fileName)
        {
            return File(Server.MapPath(@"/Images/view.jpg"), @"image/gif");
        }

        public ActionResult ToOther(string fileName)
        {
            return Redirect(@"http://localhost:1847/Menu/ShowContent");
        }
       
    }    
     
}

以上就是对.NET MVC 基本权限验证实现的分析和模拟实现。如果.net mvc 的controller不用来实现业务,还要另外提取实现业务层的情况下,那本身对Controller的Action拦截是否还有意义呢? 因为对具体 业务方法拦截才是最终目的,这导致类似AOP的拦截框架是与业务层绑定的,因此只要针对业务层实现即可了,并且还可以重用。而此时.net mvc 在提供Controller对Action 的拦截机制似乎就变得多余了。我没必要对Action拦截一次,还要对业务层具体业务再拦截一次。大家对这个问题怎么看?希望分享一下自己的经验。

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏