笔者不才看了园子里面很多园友写关于权限设计这块内容,那么笔者也在添一笔。这个是笔者在上完软件工程课程后,上交的一篇笔者论文,这里分享给大家交流,当然笔者经验尚浅,若内容有误,请大家指点出来,若大家有什么更好的想法,请提出来共同学习。
一.引言
在软件开发中, 从操作系统到一个仅仅能够发布文章的网站,都要涉及到权限的管理。在Windows 操作系统中,存在用户、组的概念。当一个用户从属于Administrators 组的时候,他就能够进行操作系统的设置与修改以及安装应用程序、修改注册表等,而当一个用户从属于Guest 组的时候,就只能浏览被允许浏览的文件和运行系统管理员允许其他运行的软件。权限系统的设计在软件中普遍存在, 但现阶段世界中并没有十分完善的权限设计方法, 只有针对具体的软件需求设计出适合的权限系统。像IBM 等世界计算机巨头都在进行权限设计方面的理论研究, 虽有一些研究成果面向大型软件公司出售,但价格十分昂贵,一般消费者难以承受。本文结合一些作者自身软件开发经验总结出一种相对比较通用点的权限设计方法,它能解决相当数量软件系统的权限设计问题。
二. 权限介绍
其实由于权限这个设计真正做到绝对通用,还是比较困难的,这个工作量很大可能需要一个团队或者一个公司来设计完成。另外一个原因不同的客户总是会提出他们自己特殊的权限需求。那么作为软件设计者,又会想法设法去完成这个需求,然而可能对于另外一个客户,又提出了一个与前者相悖的需求,那么就要去掉先前的工作,又重新设计。个人理解通用权限概括为最基本的权限,满足客户最基本的权限需求,但是也是随时随地的可以扩展。个人认为权限就是一个不断发展的过程,就像软件发展一样,是一个积累,无止境的。
1.权限的需求
这里将权限的需求做一个最基本的归纳:对资源有浏览和数据的管理。数据管理分:增加数据,删除数据,修改数据,查询数据。
每个软件都应该有这种最基本的需求。比如管理员能对数据进行增,删,查,改操作,对资源可以任意的浏览,而普通用户只能浏览限制资源,查询数据等功能。这些都包含在基本的需求当中。当然如果特殊的用户可以下载数据等这些特殊操作,这些就不在本文考虑范围之内,这都是一些可扩展工作。
2.用户
用户就是软件的使用者,凡是使用到该软件的都可以定义成用户。它应该有一些基本信息。
3.角色
角色就是软件使用者的身份,就像一个政府机构一样,什么样的身份,就有什么样的权利,而这里角色也就代表这用户的权限。
4.权限
权限就是对某一个资源是否有浏览的权利,或者对某一个数据是否有操作的权利。
5.用户组
本文中用户组就是为角色的大体类,就像社会中人的分类一样,有公务员,工人,农民,对用户更粗像的分类。除非一个系统角色很多,否则一般不会用到用户组。
三.权限设计
一般容易想得到的权限设计模式通常有两种
第一种基于角色的权限设计
这种方案是最常见也是比较简单的方案,不过通常有这种设计已经够了,所以微软就设计出这种方案的通用做法,可以看到微软在asp.net2.0中,可以直接创建用户角色及用户,它不对每一个数据操作进行权限控制,而是对角色资源访问进行控制。可以指定那些角色不能访问哪些资源等,而这些权限控制配置在web.config中。可以看到这种设计方案肯定不是通用的,因为需求完全可能提出对数据的控制。例如:销售人员不能增加商品,但是可以查询修改数据。
用户可以有多个角色,一个角色对应着多个用户,所以用户和角色之间对应关系式多对多的关系。以下是pd的CDM图
第二种就是基于操作的权限设计
这种模式下每一个操作都在数据库中有记录,用户是否拥有该操作的权限也在数据库中有记录。但是这样存在一个问题:存放这个权限记录的数据库表数据量比较大,对用户每个操作都要查询数据库,这样效率很低。
一个用户可以拥有多种权限,一种权限也对应多个用户,所以权限与数据库之间关系式多对多的关系。
本文权限设计
那么作者就集合前两种的优点,创建一种通用权限,一个用户可以拥有多个角色,一个角色可以拥有多个用户,它们之间对应关系是多对多的关系,一个角色可以拥有多个用户组,用户组的权限被角色所继承。一个用户组都有多个角色,角色和用户组的之间对应关系也是多对多的关系。首先用户组包含多个权限,不同用户组包含不同权限,用户组合权限之间关系也是多对多关系,用户组的权限的关系被用户组所属的角色所继承,那么角色与权限之间的关系也是多对多的关系。这样做数据库记录表数据量也不会很大。如果这里用户群体分类很少的话,可以将用户组去掉。
逻辑流程
权限作用的实现举例,一个用户是新闻发布者的角色,登录系统,系统会根据他账号读取他的信息,查询出他所拥有的角色,并查询角色所拥有的权限。那么这里新闻发布着角色就会查询到的权利就有新闻浏览,新闻添加,新闻查询,新闻修改。开始查询该角色是否有权利浏览新闻发布这个页面,如果没有则直接告之没有浏览权利,如果有生成该页面给其浏览,等到用户发布一条新闻时就查询用户角色是否有含有权限内容为“新闻添加”。有该权限就执行该操作,如果没有就提示该用户没有新闻发布的权限。那么查询,修改,删除都是跟增加一条新闻一样的,检查是否有权限,在看是否执行
权限存储形式:
权限ID |
权限名称 |
权限内容 |
1 |
新闻浏览 |
新闻浏览 |
2 |
新闻添加 |
新闻添加 |
… |
… |
… |
… |
… |
… |
50 |
用户添加 |
用户添加 |
51 |
用户删除 |
用户删除 |
四.权限开发
怎么开用权限,就是如何将用户数据,角色数据,用户组数据,以及权限数据添加到数据库中去。一般做法是做个相应的管理界面,由用户输入。权限数据到底包括什么,不做太复杂的考虑,就是一般最基础的权限要求,对一个资源是否有浏览权限以及对数据的操作权限,那么权限可以简单的归纳为:可浏览,增,删,查,改。那么通用的权限,那么只要开发界面管理用户,角色,以及用户组。那么对应的三个界面:用户管理界面,角色管理界面,用户组管理界面。
所有界面都应该有数据查看以及对数据的增加,修改,删除,查询等操作。具体界面看图即可。
这里个人使用一个超级管理员的角色,具有角色管理页面的浏览权利,以及增加,删除,修改,查询权利。
操作增加一个角色,打开页面点击增加,输入相应的角色名称,选择管理员用户组,此时会自动将新闻这一排权限加上,然后点击添加,就会按上述的存储方式存储数据。
操作删除一个角色,打开页面选中一个角色,删除按钮变为可以点击状态,然后点击删除即可删除。
操作修改一个角色,打开页面选中一个角色,修改按钮变为可以点击状态,然后点击修改即可修改
操作查询数据,打开页面点击查询,查询区域就显示出来,然后输入关键字即可查询了。
五.总结
权限是软件中必须要有的,如果没有权限,那么软件将会乱套,谁都可以随便操作数据,浏览资源。权限既然是开发软件必须的,我们可以从每次开发当中吸取通用的一部分,这样下次开发一个新软件时,只用稍加修改,即可满足要求。
这里没有绝对通用的权限,只有基础权限,可以在上面进行稍加修改就可以达到客户提出的权限需求,其实每个公司,每个团队,甚至个人都可能会有自己的一套通用权限。而这些都是经验的积累或者是开发的总结。有了一个通用权限以后,大家开发的周期可以减少许多,也提高了开发软件的效率,所以拥有一个自己通用权限对软件的开发,会有不少的提高。
另外一个是自己项目经验太少,以及接触到权限需求也很少,无法再短时间内设计出一个比较完整大型的通用权限系统,只能设计出比较基本的通用权限系统。也明显感觉到权限在不断的发展,客户对权限的需求在不断的提高,权限也要求分的越来越细。
六.展望
还需要不断的学习,以及项目的开发,了解更多的权限需求,制定更详细的权限操作,例如以后可以实现复制粘贴,上传下载,导入/导出Excel表格,备份数据等等更相详细的权限操作。设计出更合理的权限数据库。
参考文献:1.赵玉开-应用程序权限设计
2.金色海洋博客权限的思想
============================================
一、 描述
管理员通过控制窗体中的某个控件的Enable和visable来达到应用程序的权限控制
二、 设计思路
(一) 读取控件
将menustrip菜单选项绑定到treeview中,根据菜单选项的名称跟窗体名称相等,去
遍历出窗体中的所有form、tabctrol、button、toolstripbutton、等,
结果如下
(二) 保存控件
将treeview中显示 的控件id跟控件名称一起存入数据库,同时设置控件的可用状态,默认都可用。
(三) 将权限应用到具体窗体中
三、 数据库的设计
四、 关键部位的实现
1) 读取控件的难点
怎么样遍历到应用程序的所有窗体,这里使用了net的反射()
具体方法如下
1) TraverseForm(string Str)
public bool TraverseForm(string Str)
{
Assembly a = Assembly.LoadFile(Application.ExecutablePath);//.net中的反射
Type[] types = a.GetTypes();
foreach (Type t in types)
{
if (t.BaseType.Name == "Form" || t.BaseType.Name == "F_HKXT")
{
Form f = (Form)Activator.CreateInstance(t, true);
if (f != null)
{
// MessageBox.Show(f.Text);
if (f.Text == Str)
{
return true;
}
}
}
}
return false;
}
上面这个方法是进行判断窗体的存在性,下面这个方法是将窗体显示到树中
2) TraverseMenu(Control ctrl)
#region//遍历MenuStrip所有子菜单并显示到树上,同时判断每个子菜单下是否有子窗体,是调用TraverseFormBDTree方法将其窗体及其控件名显示到treeview中
/// <summary>
/// 遍历MenuStrip所有子菜单并显示到树上,同时判断每个子菜单下是否有子窗体,是调用TraverseFormBDTree方法将其窗体及其控件名显示到treeview中
/// </summary>
/// <param name="ctrl">是带有Menustrip的一个Form窗体</param>
public void TraverseMenu(Control ctrl)
{
foreach (Control c in ctrl.Controls)//遍历窗体中的控件
{
//toolbar1(c);
if (c is MenuStrip)//判断控件是否为Menustrip
{
MenuStrip menu = (MenuStrip)c;
foreach (ToolStripMenuItem MenuItem2 in menu.Items)//遍历menustrip的一级菜单并将其显示到树上
{
treeView1.Nodes.Add(MenuItem2.Name, MenuItem2.Text);
treeView1.Nodes[MenuItem2.Name].ToolTipText = "主菜单";
if (TraverseForm(MenuItem2.Text.Trim()) == true)//调用方法TraverseForm判断是否存在窗体
{
TraverseFormBDTree(MenuItem2.Text, treeView1.Nodes[MenuItem2.Name]);//存在调用方法TraverseFormBDTree
}
else
{
for (int i = 0; i < MenuItem2.DropDownItems.Count; i++)//遍历menustrip遍历二级菜单
{
treeView1.Nodes[MenuItem2.Name].Nodes.Add(MenuItem2.DropDownItems[i].Name, MenuItem2.DropDownItems[i].Text);
treeView1.Nodes[MenuItem2.Name].Nodes[MenuItem2.DropDownItems[i].Name].ToolTipText = "一级子菜单";
if (TraverseForm(MenuItem2.DropDownItems[i].Text) == true)
{
TraverseFormBDTree(MenuItem2.DropDownItems[i].Text, treeView1.Nodes[MenuItem2.Name].Nodes[MenuItem2.DropDownItems[i].Name]);
}
}
}
}
}
}
}
#endregion
上面方法调用了下面这个方法
3) TraverseFormBDTree(string Str, TreeNode treenode)
#region//获取当前应用程序所有的form窗体并传入菜单中的菜单显示值做为判断条件,如果传入的值可以找到相应窗体,则调用方法遍历此窗体中的所控件
/// <summary>
/// //获取当前应用程序所有的form窗体并传入菜单中的菜单显示值做为判断条件,如果传入的值可以找到相应窗体,则调用方法遍历此窗体中的所控件
/// </summary>
/// <param name="Str">这里为菜单菜单的显示值</param>
/// <param name="treenode">树的节点</param>
public void TraverseFormBDTree(string Str, TreeNode treenode)
使用这个方法后会继续调用 方法 private void TraverseFormCtrlAndBindTree(Control ctrl, TreeNode treenode)将控件名与id也绑定到树中
4) private void TraverseFormCtrlAndBindTree(Control ctrl, TreeNode treenode)
#region//递归遍历一个Form中的控件是否为button,或者是toopstrip中的toolstripbutton,如果是将基显示到树上,
/// <summary>
/// 递归遍历一个Form中的控件是否为button,或者是toopstrip中的toolstripbutton,如果是将基显示到树上,
/// </summary>
/// <param name="ctrl">窗体的控件</param>
/// <param name="treenode">一个treeview的结点</param>
private void TraverseFormCtrlAndBindTree(Control ctrl, TreeNode treenode)
下面是调用方法将其存入数据库
5) TraverseTreeAndRead(TreeNodeCollection treenode)
#region//此方法遍历树并切将其内容写到sql语句里
/// <summary>
/// 此方法遍历树并切将其内容写到sql语句里
/// </summary>
/// <param name="treenode">传入的一组TreeNodeCollection类型的树节点</param>
private void TraverseTreeAndRead(TreeNodeCollection treenode)
{
foreach (TreeNode TN in treenode)//遍历窗体中的控件
{
switch (TN.ToolTipText)
{
case "主菜单":
Code.SetControl.P_Str_MainMenuText = "主菜单" + "^" + TN.Text;
Code.SetControl.P_Str_Sub1MenuText = "Sub1Menu";
Code.SetControl.P_Str_Sub2MenuText = "Sub2Menu";
Code.SetControl.P_Str_Sub3MenuText = "Sub3Menu";
Code.SetControl.P_Str_Sub4MenuText = "Sub4Menu";
Code.SetControl.P_Str_Sub5MenuText = "Sub5Menu";
Code.SetControl.P_Str_BtnText = "P_Str_BtnText";
Code.SetControl.P_Str_TSBtnText = "P_Str_TSBtnText";
Code.SetControl.P_Str_TP1Text = "P_Str_TP1Text";
Code.SetControl.P_Str_TP2Text = "P_Str_TP2Text";
Code.SetControl.P_Str_TP1PageText = "P_Str_TP1PageText";
Code.SetControl.P_Str_TP2PageText = "P_Str_TP2PageText ";
sqlStr = "insert into d_privi_lib (login_code,app_name,main_menu,sub1_menu,sub2_menu,sub3_menu,sub4_menu,window_name,tab_name,tabpage_name,tab_name2,tabpage_name2,click_name,use_flag,p_order) " +
"values('" + Code.InitData.UserLogin.USER_CODE + "','" + Code.SetControl.P_Str_AppName + "','" + Code.SetControl.P_Str_MainMenuText + "','" + Code.SetControl.P_Str_Sub1MenuText + "','" + Code.SetControl.P_Str_Sub2MenuText + "','" + Code.SetControl.P_Str_Sub3MenuText + "','" + Code.SetControl.P_Str_Sub4MenuText + "','c_hkxt','" + Code.SetControl.P_Str_TP1Text + "','" + Code.SetControl.P_Str_TP1PageText + "','" + Code.SetControl.P_Str_TP2Text + "','" + Code.SetControl.P_Str_TSBtnText + "','" + Code.SetControl.P_Str_BtnText + "','T','123456')";
sqlStrArr[i] = EStr.EncryptString(sqlStr, Code.InitData.DESKey);
i++;
TraverseTreeAndRead(TN.Nodes);
default:
Code.SetControl.P_Str_TSBtnText = "怎么会多出这个呀!" + i.ToString();
sqlStr = "insert into d_privi_lib (login_code,app_name,main_menu,sub1_menu,sub2_menu,sub3_menu,sub4_menu,window_name,tab_name,tabpage_name,tab_name2,tabpage_name2,click_name,use_flag,p_order) " +
"values('" + Code.InitData.UserLogin.USER_CODE + "','" + Code.SetControl.P_Str_AppName + "','" + Code.SetControl.P_Str_MainMenuText + "','" + Code.SetControl.P_Str_Sub1MenuText + "','" + Code.SetControl.P_Str_Sub2MenuText + "','" + Code.SetControl.P_Str_Sub3MenuText + "','" + Code.SetControl.P_Str_Sub4MenuText + "','c_hkxt','" + Code.SetControl.P_Str_TP1Text + "','" + Code.SetControl.P_Str_TP1PageText + "','" + Code.SetControl.P_Str_TP2Text + "','" + Code.SetControl.P_Str_TSBtnText + "','" + Code.SetControl.P_Str_BtnText + "','T','123456')";
sqlStrArr[i] = EStr.EncryptString(sqlStr, Code.InitData.DESKey);
i++;
TraverseTreeAndRead(TN.Nodes);
break;
}
}
}
#endregion
1) 控件应用到窗体
这里的思路是设置当个窗体内的控件,这里应用程序所有的窗体都要继承一个父窗体,在父窗体中写方法ReturnCtrlInfo(Control ctrl)、SetButtonEnable(Control ctrl, string[,] StrArr)
最后都在load事件中调用如下 ReturnCtrlInfo(this);
对于包含菜单的页面,这另外在写个方法.
具体方法如下:
1) ReturnCtrlInfo(Control ctrl)
#region//此方法是从数据库中读取控件名,后调用方法进行窗体控件初始化
/// <summary>
/// 此方法是从数据库中读取控件名,后调用方法进行窗体控件初始化
/// </summary>
/// <param name="ctrl">控件的名称</param>
public void ReturnCtrlInfo(Control ctrl)
{
DataTable DT = new DataTable();
sqlStr = "select * from d_privi_lib where login_code='liruiba'";
DS = cser.ReturnDS1(EStr.EncryptString(sqlStr, Code.InitData.DESKey));
// DS.Tables[0].DefaultView.RowFilter = "SUB1_MENU='toolStripMenuItem4^控件权限设置'";
DS.Tables[0].DefaultView.RowFilter = "SUB1_MENU ='一级子菜单^" + ctrl.Text + "'";
DT = DS.Tables[0].DefaultView.ToTable();
string[,] StrArr = new string[DS.Tables[0].Rows.Count, 2];
for (int i = 0; i < DT.Rows.Count; i++)
{
StrArr[i, 0] = DT.Rows[i]["click_name"].ToString().Split('^').GetValue(0).ToString();//控件名称
StrArr[i, 1] = DT.Rows[i]["Use_Flag"].ToString();//控件可用标志
//MessageBox.Show(StrArr[i].ToString());
// MessageBox.Show(StrArr[i, 0].ToString() + StrArr[i, 1].ToString());
}
SetButtonEnable(ctrl, StrArr);
}
#endregion
2) SetButtonEnable(Control ctrl, string[,] StrArr)
#region//遍历控件类型,并与数据库取出的数据比较,后控件控件的可用性
/// <summary>
/////遍历控件类型,并与数据库取出的数据比较,后控件控件的可用性
/// </summary>
/// <param name="ctrl">控件的名称</param>
/// <param name="StrArr">在这里代表数据库中取出的控件名称</param>
void SetButtonEnable(Control ctrl, string[,] StrArr)
{
foreach (Control c in ctrl.Controls)//遍历窗体中的控件
{
if (c is ToolStrip) //判断是否为ToolStrip
{
ToolStrip TB = (ToolStrip)c;
for (int i = 0; i < TB.Items.Count; i++)
{
//MessageBox.Show(TB.Items[i].GetType().ToString());
if (TB.Items[i].GetType().ToString() == "System.Windows.Forms.ToolStripButton")//判断是否为ToolStripButton
{
for (int j = 0; j < StrArr.GetLength(0); j++)
{
if (StrArr[j, 0] != null)
{
// MessageBox.Show(StrArr[i, 0].ToString() + StrArr[i, 1].ToString());
if (TB.Items[i].Name == StrArr[j, 0].ToString())
{
switch (StrArr[j, 1].ToString())
{
case "T":
((ToolStripButton)TB.Items[i]).Enabled = true;
break;
case "F":
((ToolStripButton)TB.Items[i]).Enabled = false;
break;
default:
break;
}
}
}
}
}
}
}
SetButtonEnable(c, StrArr);
}
}
#endregion
五、