在权限管理中一个很重要的就是关于权限的拦截验证问题,特别是我们在webform中的验证,比纯winform要更复杂,winform可以通过验证把按钮隐藏或者禁用的方式,但是在web中我们不能仅仅通过隐藏按钮,不显示菜单/按钮之类的手段,因为客户端的代码都是透明的,如果我们不在服务端把好关,那么权限根本就无从谈起,我们必须彻底的进行验证,每一步动作都要进行验证,客户端的每一个ajax提交都要进行验证,如果任何一个ajax 动作都做过验证了,那么至少可以保证基本的安全性了.
在纯webform中,我们通常怎么来进行权限控制呢?
一般情况下,设计基类然后,在基类写好验证方法,子类调用并验证
我们来看看启航动力的开源CMS怎么进行的验证:
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using System.Web;
5: using System.Web.UI.WebControls;
6: using DTcms.Common;
7:
8: namespace DTcms.Web.UI
9: {
10: public class ManagePage : System.Web.UI.Page
11: {
12: protected internal Model.siteconfig siteConfig;
13:
14: public ManagePage()
15: {
16: this.Load += new EventHandler(ManagePage_Load);
17: siteConfig = new BLL.siteconfig().loadConfig(Utils.GetXmlMapPath("Configpath"));
18: }
19:
20: private void ManagePage_Load(object sender, EventArgs e)
21: {
22: //判断管理员是否登录
23: if (!IsAdminLogin())
24: {
25: Response.Write("<script>parent.location.href='" + siteConfig.webpath + siteConfig.webmanagepath + "/login.aspx'</script>");
26: Response.End();
27: }
28: }
29:
30: #region 管理员============================================
31: /// <summary>
32: /// 判断管理员是否已经登录(解决Session超时问题)
33: /// </summary>
34: public bool IsAdminLogin()
35: {
36: //如果Session为Null
37: if (Session[DTKeys.SESSION_ADMIN_INFO] != null)
38: {
39: return true;
40: }
41: else
42: {
43: //检查Cookies
44: string adminname = Utils.GetCookie("AdminName", "DTcms"); //解密用户名
45: string adminpwd = Utils.GetCookie("AdminPwd", "DTcms");
46: if (adminname != "" && adminpwd != "")
47: {
48: BLL.manager bll = new BLL.manager();
49: Model.manager model = bll.GetModel(adminname, adminpwd);
50: if (model != null)
51: {
52: Session[DTKeys.SESSION_ADMIN_INFO] = model;
53: return true;
54: }
55: }
56: }
57: return false;
58: }
59:
60: /// <summary>
61: /// 取得管理员信息
62: /// </summary>
63: public Model.manager GetAdminInfo()
64: {
65: if (IsAdminLogin())
66: {
67: Model.manager model = Session[DTKeys.SESSION_ADMIN_INFO] as Model.manager;
68: if (model != null)
69: {
70: return model;
71: }
72: }
73: return null;
74: }
75:
76: /// <summary>
77: /// 检查管理员权限
78: /// </summary>
79: /// <param name="channel_id">频道ID</param>
80: /// <param name="action_type">操作类型</param>
81: public void ChkAdminLevel(int channel_id, string action_type)
82: {
83: Model.manager model = GetAdminInfo();
84: BLL.manager_role bll = new BLL.manager_role();
85: bool result = bll.Exists(model.role_id, channel_id, action_type);
86: if (!result)
87: {
88: string msbox = "parent.f_errorTab(\"错误提示\", \"您没有管理该页面的权限,请勿尝试非法进入!\")";
89: //ClientScript.RegisterClientScriptBlock(Page.GetType(), "JsPrint", msbox.ToString(), true); //修正BUG
90: Response.Write("<script type=\"text/javascript\">" + msbox + "</script>");
91: Response.End();
92: }
93: }
94:
95: /// <summary>
96: /// 检查管理员权限
97: /// </summary>
98: /// <param name="channel_name">栏目名称</param>
99: /// <param name="action_type">操作类型</param>
100: public void ChkAdminLevel(string channel_name, string action_type)
101: {
102: Model.manager model = GetAdminInfo();
103: BLL.manager_role bll = new BLL.manager_role();
104: bool result = bll.Exists(model.role_id, channel_name, action_type);
105: if (!result)
106: {
107: string msbox = "parent.f_errorTab(\"错误提示\", \"您没有管理该页面的权限,请勿尝试非法进入!\")";
108: //ClientScript.RegisterClientScriptBlock(Page.GetType(), "JsPrint", msbox.ToString(), true); //修正BUG
109: Response.Write("<script type=\"text/javascript\">" + msbox + "</script>");
110: Response.End();
111: }
112: }
113:
114: /// <summary>
115: /// 检查管理员权限
116: /// </summary>
117: /// <param name="channel_name">栏目名称</param>
118: /// <param name="action_type">操作类型</param>
119: /// <returns>bool</returns>
120: public bool IsAdminLevel(string channel_name, string action_type)
121: {
122: Model.manager model = GetAdminInfo();
123: BLL.manager_role bll = new BLL.manager_role();
124: return bll.Exists(model.role_id, channel_name, action_type);
125: }
126:
127: #endregion
128:
129: #region 枚举==============================================
130:
131: /// <summary>
132: /// 统一管理操作枚举
133: /// </summary>
134: public enum ActionEnum
135: {
136: /// <summary>
137: /// 所有
138: /// </summary>
139: All,
140: /// <summary>
141: /// 查看
142: /// </summary>
143: View,
144: /// <summary>
145: /// 添加
146: /// </summary>
147: Add,
148: /// <summary>
149: /// 修改
150: /// </summary>
151: Edit,
152: /// <summary>
153: /// 删除
154: /// </summary>
155: Delete
156: }
157:
158: /// <summary>
159: /// 属性类型枚举
160: /// </summary>
161: public enum AttributeEnum
162: {
163: /// <summary>
164: /// 输入框
165: /// </summary>
166: Text,
167: /// <summary>
168: /// 下拉框
169: /// </summary>
170: Select,
171: /// <summary>
172: /// 单选框
173: /// </summary>
174: Radio,
175: /// <summary>
176: /// 复选框
177: /// </summary>
178: CheckBox
179: }
180: #endregion
181:
182: #region JS提示============================================
183:
184: /// <summary>
185: /// 添加编辑删除提示
186: /// </summary>
187: /// <param name="msgtitle">提示文字</param>
188: /// <param name="url">返回地址</param>
189: /// <param name="msgcss">CSS样式</param>
190: protected void JscriptMsg(string msgtitle, string url, string msgcss)
191: {
192: string msbox = "parent.jsprint(\"" + msgtitle + "\", \"" + url + "\", \"" + msgcss + "\")";
193: ClientScript.RegisterClientScriptBlock(Page.GetType(), "JsPrint", msbox, true);
194: }
195:
196: /// <summary>
197: /// 带回传函数的添加编辑删除提示
198: /// </summary>
199: /// <param name="msgtitle">提示文字</param>
200: /// <param name="url">返回地址</param>
201: /// <param name="msgcss">CSS样式</param>
202: /// <param name="callback">JS回调函数</param>
203: protected void JscriptMsg(string msgtitle, string url, string msgcss, string callback)
204: {
205: string msbox = "parent.jsprint(\"" + msgtitle + "\", \"" + url + "\", \"" + msgcss + "\", " + callback + ")";
206: ClientScript.RegisterClientScriptBlock(Page.GetType(), "JsPrint", msbox, true);
207: }
208: #endregion
209:
210: }
211: }
在子类校验的时候继承ManagePage的基类,然后就这样校验:
可以看到这种是通常的设计方案,基类定义,子类校验,不过这个启航动力CMS,他完全是在基类定义枚举,控制仅仅停留在增删改查,浏览,这些,不过CMS确实这一层就可以了,而且它的设计感觉不太好,因为到处都是验证代码,每一个增删改查方法都有这些验证的代码,如果是我我就会用委托的方式绑定方法,在Page_Load里面验证权限,委托绑定增删改查方法,这样,把验证的过程大大节省,这些甚至可以设计成公共的方法,来实现.
我们今天讲解的是MVC里面的权限验证,MVC天然的Controller,Action,让我们的权限控制更加容易,而且更简洁,更清晰.
首先,先来看看,MVC里面的一个小特色设计:
这个是一个普通的model,在mvc示例项目中,他采用Attribute方式来验证,我当时看到的时候感觉耳目一新,以前看<<CLR VIA C#>> Attribute的时候,当时还在想这些东西可以干什么??仅仅是元数据描述??后来才发现,这东西太强大了,控件的设计,反射,ORM,无处不在,Attributes也让代码更加优雅.
我们可以在mvc中定义各种Attribute来让我们的代码更优雅,而且让权限更简洁,比如我们经常会设计,
xxxx页面登录后才能访问,
xxxxx页面匿名用户也能访问,
xxxx页面必须经过权限验证才能访问,
xxxxx各种类型
我们可以定义各种Attribute来描述它,这种描述也让权限控制更加优雅.
比如:我们有几个登录页面,错误显示页面等等,这些页面,可以单独设计一个Attribute标记来标识.
我们可以定义各种各样的标记来描述权限控制,
可以看到我们在这里只需要标记匿名标记,然后在权限拦截中进行处理,如果存在Anonymous标记就默认允许.
这里在公共基类中设计验证标记,子类继承,这样,我们就可以达到子类全部都需要验证处理..
接下来,我们就看看,我们的权限拦截如何处理的.
我们的权限拦截是通过继承ActionFilterAttribute,自定义拦截器,这里参考传说中弦哥的思路.
简单地说,ActionFilter就是Action过滤器,任何一个请求都像筛子一样的过滤.所以,这个Filter就是筛子,我们需要的就是在这个筛子上做权限控制,这样我们的权限不就容易得多了么?
这时候,我想到了以前的一张asp.net生命周期,一个请求过来同样经过了HttpModule等一层层,在那里做权限拦截,可能效果更好,如果webform设计,可能要尝试一下看看能不能这么做.
1: /// <summary>
2: /// 权限拦截
3: /// </summary>
4: [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false)]
5: public class PermissionFilterAttribute : ActionFilterAttribute
6: {
7: /// <summary>
8: /// 权限拦截
9: /// </summary>
10: /// <param name="filterContext"></param>
11: public override void OnActionExecuting(ActionExecutingContext filterContext)
12: {
13: //权限拦截是否忽略
14: bool IsIgnored = false;
15: if (filterContext == null)
16: {
17: throw new ArgumentNullException("filterContext");
18: }
19: var path = filterContext.HttpContext.Request.Path.ToLower();
20: //获取当前配置保存起来的允许页面
21: IList<string> allowPages = ConfigSettings.GetAllAllowPage();
22: foreach (string page in allowPages)
23: {
24: if (page.ToLower() == path)
25: {
26: IsIgnored = true;
27: break;
28: }
29: }
30: if (IsIgnored)
31: return;
32: //接下来进行权限拦截与验证
33: object[] attrs = filterContext.ActionDescriptor.GetCustomAttributes(typeof(ViewPageAttribute), true);
34: var isViewPage = attrs.Length == 1;//当前Action请求是否为具体的功能页
35:
36: if (this.AuthorizeCore(filterContext) == false)//根据验证判断进行处理
37: {
38: //注:如果未登录直接在URL输入功能权限地址提示不是很友好;如果登录后输入未维护的功能权限地址,那么也可以访问,这个可能会有安全问题
39: if (isViewPage == true)
40: {
41: //跳转到登录页面
42: filterContext.RequestContext.HttpContext.Response.Redirect("~/Admin/Manage/UserLogin");
43: }
44: else
45: {
46:
47: //跳转到登录页面
48: filterContext.RequestContext.HttpContext.Response.Redirect("~/Admin/Manage/Error");
49: }
50: }
51: }
这个Attribute直接借鉴的弦哥的模型,
这样我们可以对程序进行细化的处理过程.
针对不同的标记进行处理:
1: /// <summary>
2: /// [Anonymous标记]验证是否匿名访问
3: /// </summary>
4: /// <param name="filterContext"></param>
5: /// <returns></returns>
6: public bool CheckAnonymous(ActionExecutingContext filterContext)
7: {
8: //验证是否是匿名访问的Action
9: object[] attrsAnonymous = filterContext.ActionDescriptor.GetCustomAttributes(typeof(AnonymousAttribute), true);
10: //是否是Anonymous
11: var Anonymous = attrsAnonymous.Length == 1;
12: return Anonymous;
13: }
14: /// <summary>
15: /// [LoginAllowView标记]验证是否登录就可以访问(如果已经登陆,那么不对于标识了LoginAllowView的方法就不需要验证了)
16: /// </summary>
17: /// <param name="filterContext"></param>
18: /// <returns></returns>
19: public bool CheckLoginAllowView(ActionExecutingContext filterContext)
20: {
21: //在这里允许一种情况,如果已经登陆,那么不对于标识了LoginAllowView的方法就不需要验证了
22: object[] attrs = filterContext.ActionDescriptor.GetCustomAttributes(typeof(LoginAllowViewAttribute), true);
23: //是否是LoginAllowView
24: var ViewMethod = attrs.Length == 1;
25: return ViewMethod;
26: }
27:
28: /// <summary>
29: /// //权限判断业务逻辑
30: /// </summary>
31: /// <param name="filterContext"></param>
32: /// <param name="isViewPage">是否是页面</param>
33: /// <returns></returns>
34: protected virtual bool AuthorizeCore(ActionExecutingContext filterContext)
35: {
36:
37: if (filterContext.HttpContext == null)
38: {
39: throw new ArgumentNullException("httpContext");
40: }
41: //验证当前Action是否是匿名访问Action
42: if (CheckAnonymous(filterContext))
43: return true;
44: //未登录验证
45: if (SessionHelper.Get("UserID") == null)
46: {
47: return false;
48: }
49: //验证当前Action是否是登录就可以访问的Action
50: if (CheckLoginAllowView(filterContext))
51: return true;
52:
53: //下面开始用户权限验证
54: var user = new UserService();
55: SysCurrentUser CurrentUser = new SysCurrentUser();
56: var controllerName = filterContext.RouteData.Values["controller"].ToString();
57: var actionName = filterContext.RouteData.Values["action"].ToString();
58: //如果是超级管理员,直接允许
59: if (CurrentUser.UserID == ConfigSettings.GetAdminUserID())
60: {
61: return true;
62: }
63: //如果拥有超级管理员的角色就默认全部允许
64: string AdminUserRoleID = ConfigSettings.GetAdminUserRoleID().ToString();
65: //检查当前角色组有没有超级角色
66: if (Tools.CheckStringHasValue(CurrentUser.UserRoles, ',', CurrentUser.UserRoles))
67: {
68: return true;
69: }
70:
71: //Action权限验证
72: if (controllerName.ToLower() != "manage")//如果当前Action请求为具体的功能页并且不是Manage中 Index页和Welcome页
73: {
74: //验证
75: if (!user.RoleHasOperatePermission(CurrentUser.UserRoles, controllerName, actionName))//如果验证该操作是否拥有权限
76: {
77: return false;
78: }
79: }
80: //管理页面直接允许
81: return true;
82: }
可以看到我们仅仅这一个PermissionAttribute配合其他的Attribute就实现了权限的拦截控制.
当然,我们仍然需要配置那些东西?
过滤页面.....
有一些页面我们可以直接配置允许访问的页面:
1: var path = filterContext.HttpContext.Request.Path.ToLower();
2: //获取当前配置保存起来的允许页面
3: IList<string> allowPages = ConfigSettings.GetAllAllowPage();
4: foreach (string page in allowPages)
5: {
6: if (page.ToLower() == path)
7: {
8: IsIgnored = true;
9: break;
10: }
11: }
12: if (IsIgnored)
13: return;
我们在自定义配置文件中,可以定义允许访问的页面,比如:错误页之类的,可能有些人觉得,不需要,但是这个配置是在程序发布以后,仍然能够很轻松的进行配置,所以,预留一个配置页面还是蛮有必要的.
同时我们也需要配置超级管理员身份和角色....
有人觉得,这些需要么?
我觉得是需要的,因为数据库读取出来的角色,根本没办法分辨超级管理员,只能数据库动态配置,我们就预定义这样的一个超级管理员角色的身份ID,让这个超级管理角色拥有任何的功能和任何的权限,甚至读取数据库的时候,都是读取的所有模块权限,菜单权限.
这样我们的配置工作就非常简单了,特别是数据库没有大部分配置初试信息(比如菜单信息,角色信息,)的时候,我们不需要手动往数据库添加数据,直接在程序中添加就可以了,也算是一个超级管理员身份.
接下来,我们的权限控制基本上就差不多了,现在我们可以安心的写页面了,权限神马东西,跟咱关系就不大了,这样,分工不是更爽,干别人的活才是最纠结的.......
接下来,我们可以继续设计一些公共类来简化我们日常的操作,对于公共类的设计,我觉得要拿出最大的热情与态度,要知道这些是能够真正节省我们时间的东西.
只有设计好它,我们以后才会更快更爽更轻松.
我们经常会遇到这样的代码:
1: /// <summary>
2: /// 保存资料业务
3: /// </summary>
4: /// <param name="context"></param>
5: public void SaveCompany(HttpContext context)
6: {
7: //用户json数据读取
8: company Info=new company ();
9: String CompanyStr = context.Request["Company"];
10: string id=context.Request["id"];
11: string pic = context.Request["pic"];
12: if (!Tools.IsValidInput(ref pic, true) || !Tools.IsValidInput(ref id, true))
13: {
14: return;
15: }
16: //图片保存
17: //System.IO.StreamWriter sw = new System.IO.StreamWriter(context.Server.MapPath("tzt.txt"), true);
18: //sw.Write(CompanyStr);
19: //sw.Close();
20: //使用Newtonsoft.Json.dll组件解析json对象
21:
22: JObject o = JObject.Parse(CompanyStr);
23: Info.Username = (string)o.SelectToken("Username");
24: if (!new companyBLL().CheckExistUserName(Info.Username))
25: {
26: context.Response.Write(false);
27: return;
28: }
29: Info.Password = (string)o.SelectToken("Password");
30: Info.Name = (string)o.SelectToken("Name");
31: Info.Isrecommend = ((string)o.SelectToken("Isrecommend"))=="true" ? "1" : "0";
32: Info.Fac = (string)o.SelectToken("Fac");
33: Info.Representative = (string)o.SelectToken("Representative");
34: Info.Isshow = ((string)o.SelectToken("Isshow")) == "true" ? "1" : "0";
35: Info.State = ((string)o.SelectToken("State")) == "true" ? "1" : "0";
36: Info.State1 = ((string)o.SelectToken("State1")) == "true" ? "1" : "0";
37:
38: Info.Zipcode = (string)o.SelectToken("Zipcode");
39: Info.QQ = (string)o.SelectToken("QQ");
40: Info.Telephone = (string)o.SelectToken("Telephone");
41: Info.mobilephone = (string)o.SelectToken("mobilephone");
42: Info.Email = (string)o.SelectToken("Email");
43: Info.Address = (string)o.SelectToken("Address");
44: Info.Website = (string)o.SelectToken("Website");
45: Info.Award = (string)o.SelectToken("Award");
46: Info.Introduction = (string)o.SelectToken("Introduction");
47: Info.Picturepath = pic;
48:
49: if (!string.IsNullOrEmpty(id))
50: Info.Id = Convert.ToInt32(id);
51:
52: if (!Info.Id.HasValue)
53: {
54: Info.rank = 0;
55: Info.hit = 0;
56: //执行增加操作
57: new companyBLL().AddNew(Info);
58: SMTP smtp = new SMTP(Info.Email);
59: string webpath = context.Request.Url.Scheme + "://" + context.Request.Url.Authority + System.Web.VirtualPathUtility.ToAbsolute("~/Default.aspx");
60: smtp.Activation(webpath, Info.Name);//发送激活邮件
61: }
62: else
63: {
64: new companyBLL().Update(Info);
65: }
66:
67:
68: }
这是我以前一个电子商务网站项目写的,
我现在看,简直到处都是毛病.......
首先,关于form读取内容占了一大块.......
可以想象,到处都是赋值语句,看到都吐血,再加上一些验证非空,验证函数等等,更是罪加一等的恶劣.
当然我们需要一步一步的解决这些问题,首先是剥离关于表单读取的内容,封装到一个类中,然后在类中进行验证处理.
权限验证中的样例,
验证也没有做大块的处理,不过大家可以注意,可以看到想当的简洁,我们同时需要各种辅助类的设计,强制转化的处理,非空验证的处理,等等,我们直接设计成扩展方法就能达到上图中ObjToIntNULL()的形式, 也不需要ConvertToInt32(xxxxx)的形式来转化了,不是更简洁么?
1: /* 作者: tianzh
2: * 创建时间: 2012/7/22 15:38:20
3: *
4: */
5: using System;
6: using System.Collections.Generic;
7: using System.Linq;
8: using System.Text;
9:
10: namespace TZHSWEET.Common
11: {
12: /// <summary>
13: /// 强制转化辅助类(无异常抛出)
14: /// </summary>
15: public static class ConvertHelper
16: {
17: #region 强制转化
18: /// <summary>
19: /// object转化为Bool类型
20: /// </summary>
21: /// <param name="obj"></param>
22: /// <returns></returns>
23: public static bool ObjToBool(this object obj)
24: {
25: bool flag;
26: if (obj == null)
27: {
28: return false;
29: }
30: if (obj.Equals(DBNull.Value))
31: {
32: return false;
33: }
34: return (bool.TryParse(obj.ToString(), out flag) && flag);
35: }
36: /// <summary>
37: /// object强制转化为DateTime类型(吃掉异常)
38: /// </summary>
39: /// <param name="obj"></param>
40: /// <returns></returns>
41: public static DateTime? ObjToDateNull(this object obj)
42: {
43: if (obj == null)
44: {
45: return null;
46: }
47: try
48: {
49: return new DateTime?(Convert.ToDateTime(obj));
50:
请发表评论