前言
接上面两篇 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 #endregion 102 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 #endregion 125 } 126 }
MyAuthorizeAttribute继承于AuthorizeAttribute,想要了解他的工作原理需要深入学习Asp.Net MVC的生命周期,网上已经有很多资料,在此就不赘述了。
86行的DoesSkip利用反射来判断待检测的控制器是否贴有跳过检测的标签,如有DoesSkip标签则跳过检测直接访问。这是因为在Global中注册了全局过滤器,如果对类似于登陆操作的控制器也执行权限验证,这将是一个无限的死循环。所以需要在HelperController与HomeController等控制器上添上Skip标签。
具体验证功能是怎么实现的,参考代码上的注释。
3.统一管理Session与Cookie
也许你已经注意到了,在上面的过滤器中,存在一个BussinessHelper类,其实它的名字和它的功能并没有很大的联系,只是当初在整合时用了原先的部分代码,而又没有修改类名,这个类所实现的功能只是统一管理所有的Session与Cookie信息,并没有其他复杂的业务上的操作。完整代码如下
logs_code_hide('b5a63875-da59-4390-9726-d45a4e697ffc',event)" src="/Upload/Images/2015031121/2B1B950FA3DF188F.gif" alt="" />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 set 104 { 105 Session[VCODE] = value; 106 } 107 } 108 #endregion 109 110 #region 学生对象session操作 111 /// <summary> 112 /// 学生类session 113 /// </summary> 114 public WebModel.Student StudentSession 115 { 116 get 117 { 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 //写入session 131 Session[STUDENT_INFOKEY] = student; 132 } 133 else 134 { 135 return null; 136 } 137 138 } 139 return student; 140 } 141 set 142 { 143 Session[STUDENT_INFOKEY] = value; 144 } 145 } 146 #endregion 147 148 #region 学生对象cookie操作 149 public string StudentSIDCookie 150 { 151 get 152 { 153 if (Cookies[STUDENT_INFOKEY] == null) 154 { 155 return ""; 156 } 157 else 158 { 159 return EDUA_Util.EncrypHelper.DeEncryp(Cookies[STUDENT_INFOKEY].Value.ToString()); 160 } 161 } 162 set 163 { 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 #endregion 170 171 #region 教师对象Session操作 172 public WebModel.Teacher TeacherSession 173 { 174 get 175 { 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 else 191 { 192 return null; 193 } 194 } 195 return teacher; 196 } 197 set 198 { 199 Session[TEACHER_INFOKEY] = value; 200 } 201 } 202 #endregion 203 204 #region 教师对象Cookie操作 205 public string TeacherTIDCookie 206 { 207 get 208 { 209 if (Cookies[TEACHER_INFOKEY] == null) 210 { 211 return ""; 212 } 213 else 214 { 215 return EDUA_Util.EncrypHelper.DeEncryp(Cookies[TEACHER_INFOKEY].Value.ToString()); 216 } 217 } 218 set 219 { 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 #endregion 226 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