VFP 学习、开发漫谈 (27)- 权限管理
今天与大伙儿聊一聊有关权限管理方面的话题。下面将要介绍的权限管理方案有几个前提条件:
1. 所有功能模块都是针对表单的。也就是说,应用程序中的所有功能模块,都是通过表单来实现的。我相信,这样的处理方式能够满足绝大多数用户的要求。
2. 权限管理仅针对数据维护表单或需要保密的查询表单。对于那些一般的查询表单不进行权限控制。当然,如果你精力充沛,不怕麻烦,对所有表单都进行权限控制也没有任何问题。
3. 权限共分为三种:“读”、“写”和“无”。
● “读”:是指用户可以打开表单,查看里面的数据,但不能修改数据(通过禁用“添加”、“修改”、“删除”按钮或其他控件来实现)。
● “写”:是指用户可以不受任何限制地操作表单,不但可以查询数据,也可以修改数据。
● “无”:表示用户不能打开表单,更不能查询和修改数据。实现的方法有两种:一种是禁用对应的菜单项和工具栏按钮,另一种是在表单的Init事件中查询用户对该表单的访问权限,若为“无”则Return .f.。本方案采用第一种方法。
4. 可以为“组”指定权限,也可以为“用户”指定权限。组成员自动继承组权限。当一个用户对同一个模块既拥有组权限又拥有用户权限时,忽略组权限。
5. 为了简化处理,本方案约定:每个用户仅隶属于一个组,且组之间不嵌套。
一、数据库设计
权限管理共涉及4个表:用户(user.dbf)、组(user_group.dbf)、模块(module.dbf)、权限(right.dbf)。
(一)用户表 User.dbf
说明:
● 用户名(Name):输入用户的姓名(汉字),若有重名,则后面的用户在姓名后加序号,如:“张三1”。
● 简拼(PinYin_Abb)”和“全拼(PinYin_All)”:设置这两个字段是为了在登录时,既可以输入姓名,也可以输入姓名的简拼或全拼,当离开焦点时再自动转换为用户名。
● 是否有效(Valid):用于暂停用户的登录权限。默认值是.t.
(二)用户组表 User_Group.dbf
(三)模块表 Module.dbf
说明:
● 菜单变量(mvar):表示该模块对应的菜单项,如:“2,sys”表示该模块对应“系统维护”菜单的第2个菜单项,用于根据权限启用或禁用菜单项。
● 工具栏变量(tvar):表示该模块对应的工具栏按钮,如:“cmdPorder”表示当前模块对应的工具栏按钮的名字是cmdPorder,用于根据权限启用或禁用工具栏按钮。
● 模块说明(desc):该模块的功能描述。
(四)权限表 Right.dbf
说明:
● 权限类型(user_type):用于区分当前记录所表示的权限类型,G表示组权限,U表示用户权限
● 权限(right):读,写,无。有些读者可能对这里的“无”有疑问。若不为用户分配权限,即:在权限表中无对应的记录,也可以表示权限为“无”。但是,当一个用户从组继承了某个模块的权限,而我们又想将此用户继承的组权限移除,该怎么办呢?解决方法就是再为该用户分配一个“无”权限,因为用户权限优先于组权限。这里的“无”相当于Windows 中的“拒绝”。
二、用户身份识别
在登录时,将“用户id(User_Id)”和“组id(Group_Id)”保存到全局变量 gnUserId和gnGroupId,这两个变量就是用户的身份标识,用于权限检测。为了方便用户登录,可以将本次成功登录的“用户id”保存起来,作为下次登录的默认用户。下次登录时,直接键入密码即可。
在我开发的系统中,还有一种身份识别方式:通过检测客户端的IP、MAC、计算机名,然后自动登录。这种方式适用于公司高管。因为公司高管拥有较高的审批权限,若账号被盗用,则后果严重。另一方面,若为其设置的密码过于复杂,则容易遗忘,并且让其定期更换密码也不现实。实际应用中,一个用户可以绑定多个IP、MAC、计算机名。因为多数高管都有2台电脑,一台笔记本,一台微机。
三、初始化菜单和工具栏
在成功登录系统后,应该按照权限表中的设置,启用或禁用对应的菜单项和工具栏按钮。在主程序中,通过执行 InitMenuToolbar(gnUserId,gnGroupID,oTbr) 来实现的。其中,gnUserId 为本次登录的用户id,gnGroupId为用户所属的组id,oTbr为工具栏对象。
程序代码:
* 初始化菜单、工具栏--------------------------------------------------------- FUNCTION InitMenuToolbar(nUserId,nGroupId,oToolbar) && 用户id,组id,工具栏对象 LOCAL lValue * 打开模块表,先将菜单项与工具栏按钮禁用 SELECT 0 USE module ORDER m_id SHARED SCAN FOR !DELETED() = SetMenu(ALLT(module.mvar),.T.) && 禁用菜单 = SetToolbar(oToolbar,ALLT(module.tvar),.F.)&& 禁用工具栏 ENDSCAN * 打开权限表,并设置与模块表关联 SELECT 0 USE right SHARED SET RELATION TO m_id INTO module * 先根据组权限设置菜单和工具栏 IF nGroupId # 0 SCAN FOR user_id = nGroupId AND user_type = 'G' AND !DELETED() lValue = (right # '无') = SetMenu(ALLT(module.mvar),!lValue) = SetToolbar(oToolbar,ALLT(module.tvar),lValue) ENDSCAN ENDIF * 再根据用户权限设置菜单和工具栏 SCAN FOR user_id = nUserID AND user_type = 'U' AND !DELETED() lValue = (right # '无') = SetMenu(ALLT(module.mvar),!lValue) = SetToolbar(oToolbar,ALLT(module.tvar),lValue) ENDSCAN * 关闭表 USE IN right USE IN module ENDFUNC * 子函数 - 设置菜单--------------------------------------------------------------- FUNCTION SetMenu(tcMenuName,tlValue) && 菜单名,可用状态(.T. 不可用,.F. 可用) LOCAL i,cBar,cPop i = AT(',',tcMenuName) && 【菜单条】与【弹出菜单】间以逗号分隔 IF i # 0 cBar = LEFT(tcMenuName,i-1) && 菜单条 cPop = SUBSTR(tcMenuName,i+1) && 弹出菜单 SET SKIP OF BAR &cBar OF (cPop) tlValue && 设置菜单的可用状态 ENDIF ENDFUNC * 子函数 - 设置工具条------------------------------------------------------------- FUNCTION SetToolbar(oToolBar,tcToolName,tlValue) && 工具栏,工具按钮,设置值 LOCAL cCtrl IF !EMPTY(tcToolName) AND TYPE('oToolBar.'+ tcToolName) = 'O' cCtrl = 'oToolBar.' + tcToolName &cCtrl..Enabled = tlValue ENDIF ENDFUNC
四、运行表单时检测权限
以采购订单为例,在表单的 Init 事件中,执行如下代码:
程序代码:
LOCAL cRight cRight = CheckRight('porder') && 获取用户对本表单的访问权限 DO CASE CASE cRight = '无' && 拒绝访问 MESSAGEBOX('你没有访问本模块的权限,请与管理员联系!',48,'提示') RETURN .f. CASE cRight = '读' && 只读访问:禁用“添加/修改/删除”功能 THIS.cmdAdd.Enabled = .f. THIS.cmdEdit.Enabled = .f. THIS.cmdDelete.Enabled = .f. ENDCASE
通用过程函数 CheckRight()的代码如下:
程序代码:
* 权限检查------------------------------------------------------------------- FUNCTION CheckRight(tcModule) && cModule(模块名) LOCAL nSelect,cRight,nMid,lFlag nSelect = SELECT() cRight = '无' * 找到模块 tcModule 对应的模块号 nMid IF !USED('module') USE module SHARED IN 0 lFlag = .t. ENDIF SELECT module LOCATE FOR ALLT(UPPER(Module)) == ALLT(UPPER(tcModule)) AND !DELETED() nMid = m_id IF lFlag USE IN module ENDIF * 先处理组权限 lFlag = .f. IF !USED('right') USE right SHARED IN 0 lFlag = .t. ENDIF SELECT right IF gnGroupId # 0 LOCATE FOR user_id = gnGroupId AND user_type = 'G' AND m_id = nMid AND !DELETED() IF FOUND() cRight = right ENDIF ENDIF * 再处理用户权限 LOCATE FOR user_id = gnUserId AND user_type = 'U' AND m_id = nMid AND !DELETED() IF FOUND() cRight = right ENDIF IF lFlag USE IN right ENDIF SELECT (nSelect) RETURN cRight ENDFUNC
五、权限设置
由于权限设置表单的代码很多,下面仅介绍设计思路。具体代码我已经打包,有兴趣的朋友可以下载。
Right_Manage.RAR
(14.42 KB)
1. 按“组”分配权限
通过把权限分配给用户组,则组成员自动继承这些权限,从而大大简化了权限设置的工作量。比如:仓库新来了个保管员,我只需为其新建一个用户,并将其添加到保管员组即可,无需再为其单独分配权限。
操作:首先,从上面的列表中选择一个组,则在右下方列表中显示该组所拥有的权限,而左下方列表中显示的是没有分配给该组的权限。通过中间的4个按钮或双击两个列表中的记录,则可以添加或移除权限,即:在左右两个列表中移动权限。
添加到右侧列表中的权限默认值是“读”,可以通过单击中间的“权限”列从组合框中选择其他权限。
2. 按“用户”分配权限
右下方列表中显示当前用户的所有权限,其中:红色字体显示的权限是从“组”继承过来的,不能修改。用户权限用黑色字体显示,可以修改和删除。可以通过为用户指定附加的用户权限来覆盖组权限。
这里介绍一个小技巧:对于权限列来说,红色字体的组权限不能修改,但是黑色字体的用户权限可以修改,如何才能做到这一点呢?方法是:在权限列的组合框控件的When事件中,输入:RETURN usr_b.u_type='U' 即可。当权限属于组时,When事件返回.f.,则列控件不能获得焦点,更不能修改,而When事件返回.t.时,可以正常操作。
3. 按“模块”分配权限
该方式可以查询某个功能模块哪些用户有操作权限,是前两种权限分配方式的完美补充。由于一个模块既可以分配到组,也可以分配给用户,所以较前两种分配方式稍复杂些。
[ 本帖最后由 liuxingang28 于 2014-7-26 07:32 编辑 ]