MVC中权限

一:知识点部分

权限是做网页经常要涉及到的一个知识点,在使用MVC做权限设计时需要先了解以下知识:

MVC中Url的执行是按照Controller->Action->View页面,但是我们经常需要在函数执行所指定的Action之前或者action方法之后处理一些逻辑,为了处理这些逻辑,ASP.NET MVC允许你创建action过滤器Filter,我们都知道在Action上使用的每一个 [Attribute]大都是自定义的Filter。

mvc提供四种类型的Filter接口:IActionFilter,IAuthorizationFilter,IExceptionFilter,IResultFilter, 这四种Filter足以满足我们所要实现的功能,它还提供了几个现

成的可以使用的Filter:OutputCacheAttribute、HandleErrorAttribute、AuthorizeAttribute。(AuthorizeAttribute 和 HandleErrorAttribute继承自FilterAttribute 类)

其中:

IActionFilter提供的两种方法:OnActionExecuting 在调用操作方法前调用OnActionExecuted 在调用操作方法后调用。

IResultFilter提供的两种方法:OnResultExecuting在执行由操作方法返回的操作结果前调用。OnResultExecuted在执行由操作方法返回的操作结果后调用。

IAuthorizationFilter是一个用于身份验证的Filter。只提供了一个void OnAuthorization(AuthorizationContext filterContext)方法。

IExceptionFilter会在出现异常的时候调用,也是只提供一个void OnException(ExceptionContext filterContext)的方法;

而4个接口的方法执行顺序如下:IAuthorizationFilter -> IActionFilter - >IResultFilter ->IExceptionFilter

【 ActionExecutedContext类包含一个 Canceled的属性,允许你取消当前的 Action】

现在我们来看一下用得最多的一个类ActionFilterAttribute:

他的继承层次结构为

System.Object
  System.Attribute
    System.Web.Mvc.FilterAttribute
      System.Web.Mvc.ActionFilterAttribute
        System.Web.Mvc.AsyncTimeoutAttribute
        System.Web.Mvc.OutputCacheAttribute

public abstract class ActionFilterAttribute : FilterAttribute, IActionFilter, IResultFilter{
}
他继承了 FilterAttribute, IActionFilter, IResultFilter三个类,通常我们在Action逻辑之前需要处理一些功能,比如权限等,因此会自定义一个Filter,它继承于
ActionFilterAttribute ,然后再在ActionFilterAttribute 中重载由接口 IActionFilter, IResultFilter继承下来的函数
void OnActionExecuting(ActionExecutingContext filterContext)
void OnActionExecuted(ActionExecutedContext filterContext)
void OnResultExecuting(ResultExecutingContext filterContext)
void OnResultExecuted(ResultExecutedContext filterContext) 

其中ActionFilterAttribute有两个属性,一个是继承FilterAttribute,另一个继承Attribute

Order获取或者设置执行操作筛选器的顺序。       (继承自 FilterAttribute。)

TypeId当在派生类中实现时,获取该 Attribute 的唯一标识符。       (继承自 Attribute。)

例如如下代码段:

[orderFilter(RoleId="2",Order=2)]
[UserFilter(UserId="3",Order=1)]
public ActionResult test() {
    return View();
}

程序在执行的顺序为:UserFilter->orderFilter->test

二:实例部分

了解上面的一些知识后,我们来看一下我设计的权限实例:
如果不知道用户,角色,组,权限之前的一些关系,建议你先看我上一篇“权限的基础知识”,这篇文章是我转过来的,觉得还不错,因为之前自己做完项目后没
分的很清楚,现在看完那篇文章,对号入座,才清楚原来我也是按照用户,角色,组,权限等这些关系来进行着 ^_^(如果下面对号入座的不对,请大家指正)
假设现在有4种功能权限: 添加功能、删除功能、发布功能、修改功能等。按照二进制01格式来设计,0表示没有该功能权限,1表示有该权限,
也就是说,按照顺序
          添加 删除 发布 修改 
          1      1    1     1       如果只有修改功能 就是0001,
                      如果有添加及删除功能   就是1100,

将二进制转换为int数   添加功能:(1000=int数 8) 删除功能:(0100=int数 4) 发布功能:(0010=int数 2) 修改功能:(0001 =int数 1)
            这我可以理解为 -----权限
而每个用户拥有的功能(权限)是不一样的 ,可以有多种组合, 比如1001(添加及修改权限)  0011(发布及修改权限) 1111(全部权限)
            这我可以理解为-------组
而对不同用户我将他们的权限组合功能变为int 数存于user表的Permission(int)字段中,这样的话:(添加及修改权限1001=int数 9)
(发布及修改权限0011=int 数 3) (全部权限1111=int数 15)
            这我可以理解为--------角色
至于用户就是我们自己数据库里面的username了.

设置用户权限:只需要将所拥有功能权限的int数相加存入数据库就可;
查看用户权限:则需要先将Permission的int值解析为二进制数,然后再看为1 的位数的int数值与对应的功能值就可。
实现: 当用户登录的时候,先查看他的Permission的int值(就是角色),将角色解析为相应的组,将每组的权限int值载入用户主体的Roles,他是一个数组形式。
(因为有一种或多种功能权限)
然后在每个需要进行权限过滤的Action上加上自定义的Filter:即在重载的OnActionExecuting方法内判断是否当前用户的Roles里面存在该功能权限,如果存在,
则进入页面,否则跳到无权限页面。

下面就是代码了,继承ActionFilterAttribute类,并且重载OnActionExecuting方法:

复制代码
public class RoleFilter : ActionFilterAttribute {
        public string checkRole { get; set; }     //应传入的功能权限值
        public override void OnActionExecuting(ActionExecutingContext filterContext) {
            if(!string.IsNullOrEmpty(checkRole)) {
                if(!filterContext.HttpContext.User.Identity.IsAuthenticated) {    //判断用户是否已经登录,没登录跳转到登录页面,
                    string okurl = filterContext.HttpContext.Request.RawUrl;
                    string redirectUrl = string.Format("?ReturnUrl={0}", okurl);
                    string loginUrl = FormsAuthentication.LoginUrl + redirectUrl;
                    filterContext.Result = new RedirectResult(loginUrl);
                } else {  //已登录用户
                    bool isAuthorize = filterContext.HttpContext.User.IsInRole(checkRole);  
                    if(!isAuthorize)  //判断用户是否拥有checkRole权限,没有的话跳转到权限错误页。
                        filterContext.Result = new RedirectToRouteResult("Default", new RouteValueDictionary(new { Controller = "Account", Action = "AuthorizeError" }));
                }
            } else {
                throw new InvalidOperationException("该用户没有指定角色,请联系管理员给予角色。");
            }
        }
    }
复制代码
在程序刚启动的时候如要读取用户角色的权限。才好做上面的比较:
复制代码
    public MvcApplication() {
            AuthorizeRequest += new EventHandler(MvcApplication_AuthorizeRequest);
        }

void MvcApplication_AuthorizeRequest(object sender, EventArgs e) {
            //获取当前用户的角色
            if(HttpContext.Current.User.Identity.IsAuthenticated) {
          //下面这个方法是将role(int)转换为二进制 然后算个每个权限的int值 数组
                var roles = CMSPermissionController.Instance.PermissionIdList(HttpContext.Current.User.Identity.Name.Trim()).ToArray<string>();
                HttpContext.Current.User = new System.Security.Principal.GenericPrincipal(HttpContext.Current.User.Identity, roles);
            }
        }
复制代码

调用的过程为:

复制代码
//根据需要你也可以对整个Controller加【attribute】
[RoleFilter(checkRole = "2")]
    public class GroupController : Controller {
        public ActionResult Index() {
            return View();
        }

        public ActionResult Create() {
            return View();
        }
}

//也可以对某个特定的Action添加
[RoleFilter(checkRole = "4")]
public string Delete(string RelaPath) {
//to do ...
}
复制代码

上面的代码基本上就可以实现了,在调试的过程中,曾经有个同事问,如果我要在Delete上也要检查checkRole="8"的时候你怎么办,我当时有点懵。。。没想太多,就觉得那到底是先checkRole是4还是先checkRole是“8”呢,现在看来,不存在那样状况, 因为我定义的4,8等数字, 本身就是定死的权限,比如添加 删除 发布 修改对应的权限数字是8,4,2,1;所以如果我们要对Delete本来是4的权限,来检查添加的权限8,业务逻辑上本身也是不存在的! O(∩_∩)O

 

 

 

 

 

 

服务器:网通

端口:不要禁用1234端口应该就可以访问

注意:连了数据库的,时间仓促肯定有漏洞,不要捣乱哈:)

登录用户: 1.用户名:牛头人战士 密码:000000 权限:有全部菜单页面,不能进行数据库的更改操作(不影响录入体验)

      2.用户名:老虎MM 密码:000000 权限:少两个菜单页面,不能进行数据库的更改操作(不影响录入体验)

      3.用户名:admin 密码不公开 权限:所有权限

      注:以上的实现都是通过权限管理s配置出的哈,没有任何硬编码

权限判断的边界

由于项目是基于MVC的,除去数据权限不说,功能权限的判断边界做在MVC 的Action上无疑是最好的选择,因为无论是一个页面,还是一个按钮,还是一次查询,都是通过Action请求实现的。这样我们只需要在每个Action请求执行之前进行权限判断就可以了,也不用折腾RBAC里的资源+操作=权限 这么麻烦。

菜单权限和功能权限

其实在MIS项目中,大多数的权限判断粒度还是页面级的,再加上我们还需要根据权限动态生成用户的菜单,所以我们把权限分成“菜单权限”和“功能权限”

菜单权限:在用户登录验证后,每个页面的请求都必须通过权限验证。

功能权限:默认客户进入页面后,页面的相关操作默认都不判断,只对显示维护出的功能权限进行权限判断。

这样有几个好处:一般情况下权限的配置简单了,因为只需要配置粗粒度的页面权限即可使用;增加了效率,不必每个Action执行之前都判断权限(虽然都做了缓存,但能少判断一次还是好的);完全不影响细粒度的权限判断,随时都可以增加对任何一个Action的权限判定

如何取Action功能权限

我们通过反射把所有的Action权限全部取出来,这样在维护选取的时候就比较方便了,也不会产生录入错误,如下图:

MVC中权限_第1张图片

 

大家用Demo可以体验到我们模糊输入Action名称就可以找到我们想要的Action的,因为是配置选取用也不用担心什么反射的效率问题,其实大家从demo可以看到速度还是挺快的,在我真实的项目中Action中有上万个,拉出来一样是瞬时的,所以我觉得有时候吧,也别过于“谈反射色变”,呵呵

通过反射获取所有Action的代码如下:

代码
复制代码

public IList < ActionPermission > GetAllActionByAssembly() { var result = new List < ActionPermission > (); var types = Assembly.Load( " Demo.HIS.MVC " ).GetTypes(); foreach (var type in types) { if (type.BaseType.Name == " BaseController " ) // 如果是Controller { var members = type.GetMethods(); foreach (var member in members) { if (member.ReturnType.Name == " ActionResult " ) // 如果是Action { var ap = new ActionPermission(); ap.ActionName = member.Name; ap.ControllerName = member.DeclaringType.Name.Substring( 0 , member.DeclaringType.Name.Length - 10 ); // 去掉“Controller”后缀 object [] attrs = member.GetCustomAttributes( typeof (System.ComponentModel.DescriptionAttribute), true ); if (attrs.Length > 0 ) ap.Description = (attrs[ 0 ] as System.ComponentModel.DescriptionAttribute).Description; result.Add(ap); } } } } return result; }
复制代码

返回的IList<ActionPermission>就是系统中所有Action的集合,大家可看到我们通过BaseController找到了项目中所有的Controller,再通过ActionResult找到Controller中所有的Action。

不知道大家注意下拉出的Action有个描述属性,这个属性是通过在Action上定义DescriptionAttribute实现的,这样通过反射就能取到中文描述了,例如:为了实现页面的选取方便,我们还要实现对IList<ActionPermission>的分页和模糊查询,因为是变量级集合,这里我们使用Linq查询就可以了,代码如下:


[Description( " 访问功能权限管理页面 " )] [ViewPage] public ActionResult ActionPermission() { return View(); }

代码
复制代码

public IList < ActionPermission > QueryActionPlist( string query, int start, int limit, out long total) { IList < ActionPermission > allActions = GetAllActionByAssembly(); total = (from a in allActions where a.ActionName.ToLower().Contains(query.ToLower()) select a).Count(); var result = (from a in allActions where a.ActionName.ToLower().Contains(query.ToLower()) select a).Skip(start).Take(limit); return new List < ActionPermission > (result); }
复制代码

把权限判断相关的数据都缓存起来提高效率

我们把当前登录用户的:用户信息,拥有菜单权限,拥有功能权限 放在Session里

我们把需要托管的所有Action功能权限放在 Appliction全局应用程序变量里

这样我们所有的权限相关判断都是从缓存中取数据,不需要频繁访问数据了。

相关代码懒得贴了,自己去下载的源码里翻吧....注意一下缓存相关都是通过ICache这个接口出的,搜一下就能找到

如何对每个Action进行拦截,在它执行之前判断权限

最土的办法就是在每个Action加一段权限判断的代码,哈哈...如果我要这样做的话,估计要被大家的砖头拍死。

看过本系列Asp.Net大型项目实践(7)-用Unity实现AOP之事务处理+为啥要用AOP(附源码)的朋友应该就能想到,这是一个典型的AOP应用场景。

由于Asp.net MVC的Filter机制其实就是Aop,所以我们直接使用它。熟悉Asp.net MVC的朋友估计知道里面其实自带的有一个AuthorizeAttribute的ActionFilter,但基本就是个玩具,本来我想继承它重写的,但无奈里面的filterContext没有ActionDescriptor属性,所以干脆不要它自己写个ActionFilter,代码如下:

代码
复制代码

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Web.Mvc; using System.Web; using System.Security.Principal; using Demo.HIS.Infrastructure.Facade.Authority; namespace Demo.HIS.MVC.CommonSupport.Filter { /// /// 权限拦截 /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false )] public class AuthorizeFilterAttribute : ActionFilterAttribute { public override void OnActionExecuting(ActionExecutingContext filterContext) { if (filterContext == null ) { throw new ArgumentNullException( " filterContext " ); } var path = filterContext.HttpContext.Request.Path.ToLower(); if (path == " / " || path == " /Main/Login " .ToLower() || path == " /Main/UserLogin " .ToLower()) return ; // 忽略对Login登录页的权限判定 object [] attrs = filterContext.ActionDescriptor.GetCustomAttributes( typeof (ViewPageAttribute), true ); var isViewPage = attrs.Length == 1 ; // 当前Action请求是否为具体的功能页 if ( this .AuthorizeCore(filterContext, isViewPage) == false ) // 根据验证判断进行处理 { // 注:如果未登录直接在URL输入功能权限地址提示不是很友好;如果登录后输入未维护的功能权限地址,那么也可以访问,这个可能会有安全问题 if (isViewPage == true ) { filterContext.Result = new HttpUnauthorizedResult(); // 直接URL输入的页面地址跳转到登陆页 } else { filterContext.Result = new ContentResult { Content = @" JsHelper.ShowError('抱歉,你不具有当前操作的权限!') " }; // 功能权限弹出提示框 } } } // 权限判断业务逻辑 protected virtual bool AuthorizeCore(ActionExecutingContext filterContext, bool isViewPage) { if (filterContext.HttpContext == null ) { throw new ArgumentNullException( " httpContext " ); } if ( ! filterContext.HttpContext.User.Identity.IsAuthenticated) { return false ; // 判定用户是否登录 } var user = new CurrentUser(); // 获取当前用户信息 var controllerName = filterContext.RouteData.Values[ " controller " ].ToString(); var actionName = filterContext.RouteData.Values[ " action " ].ToString(); if (isViewPage && (controllerName.ToLower() != " main " && actionName.ToLower() != " masterpage " )) // 如果当前Action请求为具体的功能页并且不是MasterPage页 { if (user.MenuPermission.Count(m => m.ControllerName == controllerName && m.ActionName == actionName) == 0 ) return false ; } else { var actions = ContainerFactory.GetContainer().Resolve < IAuthorityFacade > ().GetAllActionPermission(); // 所有被维护的Action权限 if (actions.Count(a => a.ControllerName == controllerName && a.ActionName == actionName) != 0 ) // 如果当前Action属于被维护的Action权限 { if (user.ActionPermission.Count(a => a.ControllerName == controllerName && a.ActionName == actionName) == 0 ) return false ; } } return true ; } } }
复制代码

a.我的AuthorizeFilterAttribute继承了ActionFilterAttribute

b.重写ActionFilterAttribute的OnActionExecuting(ActionExecutingContext filterContext)方法,这个方法表示在每个Action执行之前处理

c.返回页面的Action上面要加ViewPageAttribute,这样我在Filter里才能区分当前Action是否为页面(注:由于是在Action执行之前,无法通过获取ActionResult的类型来判断)

d.权限判断和处理的逻辑自己看贴的代码里的注释,已经写的很清楚了

在哪儿加这个AuthorizeFilterAttribute呢?一个一个给Action或Controller加都太麻烦了,还记得我们所有的Controller都继承了BaseController吗?在他上面就完事了呗!这样整个系统要不权限就靠BaseController上的一个AuthorizeFilterAttribute说了算~,BaseController如下:

复制代码

namespace Demo.HIS.MVC.CommonSupport { // 所有Controller都要继承的基类 [AuthorizeFilter] [ExceFilter] public class BaseController : Controller { } }
复制代码

使用FormsAuthentication实现登录验证

用户登录验证用的是.Net自带的Forms验证,它默认通过加密的cookies来实现用户的登录判断

登录代码如下:

代码
复制代码

[ExtResult] [Description( " 用户登录 " )] public ActionResult UserLogin() { string loginName = Request[ " loginname " ]; string pwd = HospUser.GetPwdMD5(Request[ " pwd " ]); if ( ! AuthorityFacade.Validate(loginName, pwd)) throw new ValidationException( " 用户名密码错误,或用户状态不可用 " ); FormsAuthentication.SetAuthCookie(loginName, true ); // 加入from验证票据 Cache.RemoveSessionCache( " currentuser " ); // 清空当前用户信息缓存 return this .JsonFormat( new ExtResult { success = true }); }

注销代码如下:

代码

// 注销登陆 public ActionResult Logout() { FormsAuthentication.SignOut(); Cache.RemoveSessionCache( " currentuser " ); // 清空当前用户信息缓存 return this .JsonFormat( new ExtResult { success = true }); }

大伙期待的源码

虽然是从公司实际项目中扒出来的,但整个设计,思想,架构和关键代码基本都是我一个人搞的,而且业务相关的东西我都去掉了所以大家可以随便用。

里面可能涉及到不少东西还没有讲到,在以后的系列中会详细说明。

另外有人说源码编译报错缺少“HibernatingRhinos.NHibernate.Profiler.Appender.DLL”,你可以把这个DLL删除,再根据错误信息删除一行代码即可,这个其实是之前讲过的NHProfiler,NH生成SQL查看工具用的DLL。

源码:HISDemo-10.rar

数据库(估计用SQLSERVER的比较多,所以我把数据库从oracle改成了SqlServer2005,直接附加就可以了):HISDemoDb.rar

总结

  权限管理这部分暂时就介绍到这里 ,以后有机会再给大家介绍数据权限,权限与工作流引擎集成,单点登录,多级授权等复杂的权限管理实现。大家可以看到上面介绍权限管理还是有些特点的:

1.充分利用MVC特点;

2.思路清晰;

3.验证逻辑简单;

4.维护方便;

5.没有效率问题;

6.和其他代码完全解耦毫无依赖;

7.非常灵活能满足绝大多数需求;

8.扩展方便;

我想通过这个权限管理的例子和相关讨论,大家应该明白一个道理,只有真正适合自己当前项目的业务和技术特点的技术方案才是好方案。

在技术水平有限的情况下盲目追求什么通用,什么全适应,往往实现费时费力还不讨好。

在软件开发这个领域,真正的高手不在于写高深的代码;玩弄艰涩的术语;也不会不考虑实际价值,为了用技术而用技术;也不会胡吹海吹,堕落到为了赚钱就放弃技术。

理论和实践结合才是王道

还是那两句老话,软件开发没有银弹,一切为了需求

你可能感兴趣的:(asp.net)