你的位置:首页 > ASP.net教程

[ASP.net教程]2_MVC+EF+Autofac(dbfirst)轻型项目框架_用户权限验证


前言

  接上面两篇 0_MVC+EF+Autofac(dbfirst)轻型项目框架_基本框架 与 1_MVC+EF+Autofac(dbfirst)轻型项目框架_core层(以登陆为例) 。在第一篇中介绍了此架构的基本分层,在第二篇中,以登陆功能为例,介绍了项目的代码结构。在本篇中将通过过滤器实现用户权限验证功能。

  同样,文中有问题的地方欢迎批评指正!谢谢!

开发背景 

  在一个常规系统中权限验证是不可缺的,在较简单的系统中,用户只会被简单归为登陆用户和游客,而在较为复杂的系统中,除了判断用户是否登陆外,还需提供一套可靠的机制来验证用户是否拥有执行其请求的操作的权限。在ASP.Net MVC框架中的AuthorizeAttribute过滤器很好的满足了这个要求。在我项目中,也是通过AuthorizeAttribute来实现对用户权限的判断。其中为了便捷,引入了Helper类来统一管理所有的Session和Cookie。

创建过程

 1.权限验证模式与表结构

  MVC框架中存在的控制器提供了很好的权限范围的单位,所以在我的框架中,权限是验证是以过滤器为基础的,一个过滤器代表一个权限,不同的身份拥有不同的权限集合,而每一个用户归属于一个身份。例如用户甲的身份为超级管理员身份,他可能同时具有A,B,C,D,E,F这六个权限;而权限相对较低的用户乙为普通管理员身份,他可能仅仅具备A,B,C,D这四个权限。

  数据库的表结构如下图:

  可以看到,在教师表中存在一个字段用来标志对应的Power的ID(PID),power表记录所有身份(power这个词用的有点怪 :-)),Authority表中记录了所有权限,解释下Authority中每一个字段的具体含义:

    AUID:该权限的对应ID。

    AUPID:该权限隶属的父权限,例如:学生管理,教师管理的父级权限可能为管理中心。

    Name:权限的名字。

    AOrder:为了方便将权限转化为用户可见的控件操作树,安排权限的排名先后顺序。

    URL:这个权限所对应打开的URL。

    Controller:这个权限对应的控制器。

  Power与Authority之间的AuthorityToPower映射了每一个身份所拥有的权限。

 2.AuthorizeAttribute过滤器

  在介绍AuthorizeAttribute之前有必要先介绍下我web的结构,通过下图可以看到在根路由两个控制器的基础上,我还拥有两个区域,分别为学生和教师。所以在AuthorizeAttribute过滤器中,通过判断区域名来实施具体的权限验证操作。结合业务逻辑,在学生区域下,过滤器的功能仅仅是判断学生的登陆状态,如未登陆跳转到登陆界面;而在教师区域下,除了要判断是否登陆外还要判断其所请求的控制器是否在他的权限集合中。

  AuthorizeAttribute中代码如下

 1 using EDUA_ICore; 2 using System; 3 using System.Collections.Generic; 4 using System.Linq; 5 using System.Web; 6 using System.Web.Mvc; 7  8 namespace EDUA_WEB.Filters 9 { 10   /// <summary> 11   /// 身份(权限)过滤器 12   /// </summary> 13   public class MyAuthorizeAttribute:AuthorizeAttribute 14   { 15     /// <summary> 16     /// core操作上下文 17     /// </summary> 18     private ICoreSession iCoreSession; 19  20     #region 构造方法 传入过滤器 21     public MyAuthorizeAttribute(ICoreSession iCoreSession) 22     { 23       this.iCoreSession = iCoreSession; 24     }  25     #endregion 26  27     #region 重写权限验证器 28     public override void OnAuthorization(AuthorizationContext filterContext) 29     { 30       //检测是否贴有跳过标签 31       if (!DoesSkip<Filters.SkipAttribute>(filterContext)) 32       { 33         //获取区域名 34         string strArea = "AreaIsNull"; 35         if (filterContext.RouteData.DataTokens.Count > 0) 36         { 37           strArea = filterContext.RouteData.DataTokens["area"].ToString().ToLower(); 38         } 39         //获取控制器名 40         string strController = filterContext.RouteData.Values["controller"].ToString().ToLower(); 41         //获取方法名 42         string strAction = filterContext.RouteData.Values["action"].ToString().ToLower(); 43  44         //filterContext.HttpContext.Response.Write(strArea + "--" + strController + "--" + strAction); 45         OperateHelper.BussinessHelper h = new OperateHelper.BussinessHelper(iCoreSession); 46         //学生域之下 47         if (strArea == "student") 48         { 49           if (h.StudentSession == null) 50           { 51             filterContext.Result = new RedirectResult("/home/login");  52           } 53         } 54         //教师域之下 55         else if (strArea == "teacher") 56         { 57           if (h.TeacherSession == null) 58           { 59             //如果在主页控制器下 则直接跳转 其他 提示过期  60             if (strController == "teacherhome") 61             { 62               //filterContext.HttpContext.Response.Redirect("/home/login"); 63               filterContext.Result = new RedirectResult("/home/login");  64             } 65             else 66             { 67               filterContext.HttpContext.Response.Write("登陆已过期,请刷新页面"); 68               filterContext.HttpContext.Response.End(); 69             } 70  71           } 72           else 73           { 74             //判断是否对应权限 75             if (!CheckPermission(strController, h.TeacherSession.AuthorityList)) 76             { 77               filterContext.HttpContext.Response.Write("你的请求超出了你的权限,如修改过系统权限,请重新登录系统"); 78               filterContext.HttpContext.Response.End(); 79             } 80           } 81         } 82       } 83     }  84     #endregion 85  86     #region 检测是否贴有某标签 + bool DoesSkip<T>(AuthorizationContext filterContext) where T : Attribute 87     /// <summary> 88     /// 检测是否贴有某标签 89     /// </summary> 90     /// <typeparam name="T">标签type</typeparam> 91     /// <param name="filterContext">上下文</param> 92     /// <returns>是否贴有标签</returns> 93     bool DoesSkip<T>(AuthorizationContext filterContext) where T : Attribute 94     { 95       if (!filterContext.ActionDescriptor.IsDefined(typeof(T), false) && !filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(T), false)) 96       { 97         return false; 98       } 99       return true;100     }101     #endregion102 103     #region 检验访问的管理员控制器是否与权限对应 + bool CheckPermission(string strController, List<WebModel.Authority> aul)104     /// <summary>105     /// 检验访问的管理员控制器是否与权限对应106     /// </summary>107     /// <param name="filterContext">控制器名</param>108     /// <param name="permission">权限列表</param>109     /// <returns>是否有权限</returns>110     private bool CheckPermission(string strController, List<WebModel.Authority> aul)111     {112       bool ret = false;113       //将请求的控制器名与权限session中的控制器表进行匹配 如果有,则匹配通过114       for (int i = 0; i < aul.Count(); i++)115       {116         if (aul[i].Controller.ToLower() == strController)117         {118           ret = true;119           break;120         }121       }122       return ret;123     } 124     #endregion125   }126 }

   MyAuthorizeAttribute继承于AuthorizeAttribute,想要了解他的工作原理需要深入学习Asp.Net MVC的生命周期,网上已经有很多资料,在此就不赘述了。

  86行的DoesSkip利用反射来判断待检测的控制器是否贴有跳过检测的标签,如有DoesSkip标签则跳过检测直接访问。这是因为在Global中注册了全局过滤器,如果对类似于登陆操作的控制器也执行权限验证,这将是一个无限的死循环。所以需要在HelperController与HomeController等控制器上添上Skip标签。

  具体验证功能是怎么实现的,参考代码上的注释。

3.统一管理Session与Cookie

  也许你已经注意到了,在上面的过滤器中,存在一个BussinessHelper类,其实它的名字和它的功能并没有很大的联系,只是当初在整合时用了原先的部分代码,而又没有修改类名,这个类所实现的功能只是统一管理所有的Session与Cookie信息,并没有其他复杂的业务上的操作。完整代码如下

 1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Web; 5 using System.Web.SessionState; 6  7  8 namespace EDUA_WEB.OperateHelper 9 { 10   public class BussinessHelper 11   { 12     #region 对象保存名称 13     /// <summary> 14     /// 验证码保存名 15     /// </summary> 16     const string VCODE = "vcode"; 17     /// <summary> 18     /// 教师信息保存名 19     /// </summary> 20     const string TEACHER_INFOKEY = "tinfo"; 21     /// <summary> 22     /// 学生信息保存名 23     /// </summary> 24     const string STUDENT_INFOKEY = "sinfo";  25     #endregion 26  27     #region 构造方法 28     public BussinessHelper() { } 29  30     /// <summary> 31     /// 构造方法 32     /// </summary> 33     /// <param name="coreSession">业务操作对象</param> 34     public BussinessHelper(EDUA_ICore.ICoreSession coreSession) 35     { 36       this.iCoreSession = coreSession; 37     }  38     #endregion 39  40     #region http上下文的对象们 41     /// <summary> 42     /// 业务操作对象 43     /// </summary> 44     private EDUA_ICore.ICoreSession iCoreSession { get; set; } 45  46     /// <summary> 47     /// 当前Http上下文 48     /// </summary> 49     private HttpContext ContextHttp 50     { 51       get 52       { 53         return HttpContext.Current; 54       } 55     } 56  57     /// <summary> 58     /// session对象 59     /// </summary> 60     private HttpSessionState Session 61     { 62       get 63       { 64         return ContextHttp.Session; 65       } 66     } 67  68     /// <summary> 69     /// Cookie对象 70     /// </summary> 71     private HttpCookieCollection Cookies 72     { 73       get 74       { 75         return ContextHttp.Request.Cookies; 76       } 77     } 78  79     /// <summary> 80     /// Response 对象 81     /// </summary> 82     HttpResponse Response 83     { 84       get 85       { 86         return ContextHttp.Response; 87       } 88     }  89     #endregion 90  91     #region 验证码session对象 92     /// <summary> 93     /// 验证码session设置 94     /// </summary> 95     public string Vcode 96     { 97       get 98       { 99         if (Session[VCODE] == null)100           return "";101         return Session[VCODE].ToString();102       }103       set104       {105         Session[VCODE] = value;106       }107     } 108     #endregion109 110     #region 学生对象session操作111     /// <summary>112     /// 学生类session113     /// </summary>114     public WebModel.Student StudentSession115     {116       get117       {118         WebModel.Student student = Session[STUDENT_INFOKEY] as WebModel.Student;119         if (student == null)120         {121           string id = this.StudentSIDCookie;122           if (id == "")123           {124             return null;125           }126           WebModel.ReturnVal rv = iCoreSession.IStudent.GetStudentBySID(id);127           if (rv.Statu == WebModel.ReturnStatu.Success)128           {129             student = rv.Data as WebModel.Student;130             //写入session131             Session[STUDENT_INFOKEY] = student;132           }133           else134           {135             return null;136           }137 138         }139         return student;140       }141       set142       {143         Session[STUDENT_INFOKEY] = value;144       }145     } 146     #endregion147 148     #region 学生对象cookie操作149     public string StudentSIDCookie150     {151       get152       {153         if (Cookies[STUDENT_INFOKEY] == null)154         {155           return "";156         }157         else158         {159           return EDUA_Util.EncrypHelper.DeEncryp(Cookies[STUDENT_INFOKEY].Value.ToString());160         }161       }162       set163       {164         HttpCookie cookie = new HttpCookie(STUDENT_INFOKEY, EDUA_Util.EncrypHelper.ToEncryp(value.ToString()));165         cookie.Expires = DateTime.Now.AddHours(2);166         Response.Cookies.Add(cookie);167       }168     } 169     #endregion170 171     #region 教师对象Session操作172     public WebModel.Teacher TeacherSession173     {174       get175       {176         WebModel.Teacher teacher = Session[TEACHER_INFOKEY] as WebModel.Teacher;177         if (teacher == null)178         {179           string id = this.TeacherTIDCookie;180           if (id == "")181           {182             return null;183           }184           WebModel.ReturnVal rv = iCoreSession.ITeacher.GetTeacherByID(id);185           if (rv.Statu == WebModel.ReturnStatu.Success)186           {187             teacher = rv.Data as WebModel.Teacher;188             Session[TEACHER_INFOKEY] = teacher;189           }190           else191           {192             return null;193           }194         }195         return teacher;196       }197       set198       {199         Session[TEACHER_INFOKEY] = value;200       }201     } 202     #endregion203 204     #region 教师对象Cookie操作205     public string TeacherTIDCookie206     {207       get208       {209         if (Cookies[TEACHER_INFOKEY] == null)210         {211           return "";212         }213         else214         {215           return EDUA_Util.EncrypHelper.DeEncryp(Cookies[TEACHER_INFOKEY].Value.ToString());216         }217       }218       set219       {220         HttpCookie cookie = new HttpCookie(TEACHER_INFOKEY, EDUA_Util.EncrypHelper.ToEncryp(value.ToString()));221         cookie.Expires = DateTime.Now.AddHours(2);222         Response.Cookies.Add(cookie);223       }224     } 225     #endregion226 227   }228 }

View Code

  它的构造方法需要传入核心类实例,因为当保存在客户端的Session过期,而用户又保存了Cookie的情况下,需要通过对加密的Cookie解密来产生新的session保存并返回。所以这里需要在Core中的teacher和student中添加对应的根据GUID返回教师(学生)实体的方法。

  在前一篇登陆操作中,也使用了这个类,所要说明的是在teacher对象中,教师的权限列表在登陆时被取出放入了Session中,可以防止每一次验证连接数据库,减轻数据库负担。

4.生成对应权限菜单(树)

  这里我采用了EasyUI的异步加载来生成权限访问列表,效果如下图

  在它的左侧为其对应权限列表映射的权限树。对应生成权限树的控制器代码如下

1     #region 1.1 生成左侧菜单2     public ActionResult GetMenuData()3     {4       OperateHelper.BussinessHelper h = new OperateHelper.BussinessHelper();5       List<WebModel.Authority> authList = h.TeacherSession.AuthorityList;6       List<WebModel.EasyUIModel.TreeNode> tl = WebModel.EasyUIModel.TreeNode.getTreeNodeListByAuModelList(authList);7       return Content(EDUA_Util.WEB.DataHelper.Obj2Json(tl));8     } 9     #endregion

 

 

  可以看到,它的权限列表也是从session中取出,并不需要再次连接数据库。具体的easyui的代码因为不涉及到框架本身,所以我就不提供了。

写在最后

  到这里,一个轻型框架已经可使用了。

  最后说下关于分享源代码的问题:

  我当初写这三篇博客的出发点:第一是为了理清我的思路,第二为将来可能接手系统的同学提供扩展开发时的参考,所以我并没有把这些文章分享到博客园的首页。但还是谢谢一些关注到这些文章的朋友。有一些朋友通过微博等问我能不能给下源代码,其实大多数的核心的代码在以上三篇文章中都已经给出了,如果仔细看完一定是能形成框架。至于完整的源代码,因为已经存在不少的业务上的内容,重新整理需要不少时间,也不切实际,所以恕我不能打包上传了,但还是谢谢大家的关注。如果有需要交流的,可以私信我的新浪微博@导弹林瀚,再次谢谢大家!

转载请注明出处 huhuhuo的博客园

地址:http://www.cnblogs.com/linhan/p/4320862.html