2 菜单管理
菜单管理主要包括两部分的内容,菜单项的编辑维护与根据当前登陆用户的权限进行菜单的生成。
菜单的编辑维护就是指对菜单表的维护。因为比较简单,在此不做详细说明,只是要注意以下三个问题:
第一、菜单编号与权限位数的生成。因为菜单项是可以删除的,如果新增菜单项获取编号与权限位数时只是在最大的编号上增1的话,必须会导致编码资源的浪费,特别是权限位数,只有100位,如果由于跳号而导致了编码资源的浪费,必然导致新功能模块无编码资源可用。所以,在添加新菜单项是必须要解决跳号的问题。请看以下取编号与权限位数的语句:
select min(CDBH)+1 from CDB where CDBH+1 not in(select CDBH from CDB)
select min(QXWS)+1 from CDB where QXWS+1 not in(select QXWS from CDB)
我们只要保证对于菜单维护本身的这个功能模块给它分配菜单编号为1,权限位数为1的话,就能够保证在任何情况下新增菜单项时菜单编号与权限位数不会跳号,编码资源能够得到充分利用。因为菜单维护本身这个菜单项在通常情况下是不会被删除的。
第二、入口路径是指进入本功能模块的第一个页面的连接地址,请注意路径的相对性问题。
第三、相关文件是指完成本功能模块的页面组合。一般情况下,一个菜单项下连接一个功能模块,要完成该功能模块的功能可能需要若干个ASP页面。例如,菜单项1的入口路径是../menu1.asp,还要以下三个页面,menu11.asp, menu12.asp, menu13.asp,则相关文件的内容是指meunu1.asp, menu11.asp,menu12.asp,menu13.asp这个字符串序例。该项的主要作用是为了安全检查而设置的,也就是页面在系统中的注册过程。
根据当前登陆用户的权限进行菜单生成时有一个关键的会话变量:Session("YHQX"),该变量在后面将要进行描述的安全检查过程中生成,一直保存到用户退出系统或者会话超时失效时为止。里面的内容就是100位的当前登陆用户的权限字符串。根据该变量生成菜单的过程如下:
<% sql="select * from CDB where FCDBH=0 order by CDBH"
set Rs=Conn.execute(sql)
'取得菜单表中的所有有子菜单的父菜单
'遍历父菜单
do while not Rs.eof
SubMenuNum=0
sql="select QXWS from CDB where FCDBH="&Rs("CDBH")&" order by CDBH"
set Rs1=Conn.execute(sql)
'获取当前父菜单下的所有子菜单
'遍历当前父菜单下的子菜单
do while not Rs1.eof
if Mid(Session("YHQX"),Rs1("QXWS"),1)=1 then
SubMenuNum=SubMenuNum+1
'如果当前用户有当前子菜单的权限,则子菜单的个数加上1
end if
Rs1.movenext
loop
Rs1.close
set Rs1=nothing
if SubMenuNum>0 then
'如果当前用户的当前父菜单的下子菜单个数大于零则生成该父菜单
if Rs.bof then FirstMenu=Rs("CDBH") end if
MenuNum=Rs("CDBH")
ParentName=Rs("CDMC")
response.write "<!-- "&ParentName&"父菜单 -->"
%>
<menu class=parent name=<%=ParentName%> url=<%=Rs("RKLJ")%> target= mainFrame >
'菜单的形式与样式根据需要自己调整,这里采用泛指方式
<%
response.write "<!-- "&ParentName&"子菜单 -->"
sql="select * from CDB where FCDBH="&MenuNum&" order by CDBH"
set Rs1=Conn.execute(sql)
'查找该父菜单下的所有子菜单
do while not Rs1.eof
if Mid(Session("YHQX"),Rs1("YHQX"),1)=1 then
%>
<menu class=child name=<%=Rs("CDMC")%> url=<%=Rs("RKLJ")%> target= mainFrame ><br>
'菜单的形式与样式根据需要自己调整,这里采用泛指方式
<%
end if
Rs1.movenext
loop
Rs1.close
set Rs1=nothing
end if
Rs.movenext
loop
Rs.close
set Rs=nothing
%>
由于本系统只有两级菜单,所以没有牵涉到菜单递归调用生成的问题。如果要实现的话必须把菜单生成过程定义成函数形式,才能进行递归调用,在此不做详细讨论。
3 权限分配
权限的分配由用户组权限分配与用户的权限分配两部分组成。是将具体的菜单与用户组或者用户关联的过程。
首先看用户组的添加界面如下:
<form name="zjyhz" method="post" action="yhzbmb_zj.asp" onsubmit="return checkinput(this)"> <!-- 调用输入检验函数,并生成权限字符串-->
<input type="hidden" name="action" value="zjyhz">
<table cellpadding="0" cellspacing="0" width="100%">
<tr>
<td><fieldset align="center"><legend>增加用户组
</legend>
<input type="hidden" name="DQDWDM" value="<%=DQDWDM%>"><!--由前一页传入,当前单位编码-->
用户组名称:<input type="text" name="yhzmc" size="50" maxlength="50" ><br><br>
设置用户组权限:<br>
<table width="100%" border="0" cellpadding="0" cellspacing="0">
<tr>
<td width="150">
<input type="hidden" name="yhzqx">
<select id="allqx" size=10 style='width:150'>
<%
sql="SELECT CDMC, QXWS FROM CDB where FCDBH<>0 ORDER BY CDBH"
set Rs1=Conn.exece(sql)
'取得所有子菜单
%>
<%do while not Rs1.eof
if Mid(Session("YHQX"),Rs1("QXWS"),1)=1 then
'但是只有当前用户拥有的权限它才能将该权限赋给它所新建立的用户组,有利的保证了安全性,只能继承,不能超越,将满足条件的菜单项显示在例表框中,待用户选择。
%>
<option value="<%=Rs1("QXWS")%>"><%=Rs1("CDMC")%></option>
<%
end if
Rs1.movenext
loop
Rs1.close
set Rs1=nothing
%>
</select>
</td>
<td width="30" align="center">
<input type="button" name="Submit3" value=" -> " class="addbutton" onclick="MoveItem(document.zjyhz.allqx,document.zjyhz.selectqx)">
<input type="button" name="Submit32" value=" <- " class="addbutton" onclick="MoveItem(document.zjyhz.selectqx,document.zjyhz.allqx)">
</td>
<td width="*">
<!—已经选好的菜单项的例表框-->
<select id="selectqx" size=10 style='width:150'>
</select>
</td>
</tr>
</table><br>
<input type="submit" name="Submit" value="增 加">
<input type="reset" name="Submit2" value="清 空">
<input type="button" name="Submit2" value="放 弃" onclick="location.href='yhzbmb.asp?DQDWDM=<%=DQDWDM%>'">
</fieldset></td>
</tr>
</table>
</form>
<!—以下javascript函数实现界面的操作以及输入验证工作-->
<script language="javascript">
//在一个例表框中插入一项
function InsertItem(ListObj,ListText,ListValue)
{ len=ListObj.length;
ListObj.options[len] = new Option(ListText,ListValue);}
//将例表框中指定的项删除
function DeleteItem(ListObj,DeleteIndex)
{ ListObj.options[DeleteIndex] = null;}
//将一项从一个例表框移到另一个例表框
function MoveItem(srcListObj,decListObj)
{ if(srcListObj.selectedIndex!=-1)
{ ListValue=srcListObj.options[srcListObj.selectedIndex].value; ListText=srcListObj.options[srcListObj.selectedIndex].text;
DeleteItem(srcListObj,srcListObj.selectedIndex);
for(i=0;i<decListObj.length;i++)
{ if(ListValue==decListObj.options[i].value) return}
InsertItem(decListObj,ListText,ListValue);}
}
//输入校验并生成新用户组权限字符串
function checkinput(FormHandle)
{ if(FormHandle.yhzmc.value=="")
{ alert("用户组名称不能为空!");
FormHandle.yhzmc.focus();
return false; }
var strYhzqx="0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000";
var beforebit,afterbit
for(i=0;i<FormHandle.selectqx.length;i++)
{ beforebit=strYhzqx.substr(0,FormHandle.selectqx.options[i].value-1);
afterbit=strYhzqx.substr(FormHandle.selectqx.options[i].value,99);
strYhzqx=beforebit+"1"+afterbit; }
if(strYhzqx.length!=100) alert("bit error");
FormHandle.yhzqx.value=strYhzqx;
return true;}
</script>
以上FORM提交后写入数据库就完成了用户组的添加,这里也有用户组编号可能跳号的问题,可以用上面已经描述过的方法加以解决。当对用户组的权限进行编辑时,我们必须将属于当前登陆用户但不属于该用户组的权限显示在一个例表框中,属于该用户组的权限显示在别一个例表框中,用以下语句分别实现:
<select id="allqx" size=10 style='width:150'>
<%sql="SELECT CDMC, QXWS FROM CDB where FCDBH<>0 ORDER BY CDBH"
set Rs1=Conn.execute(sql) %>
<%do while not Rs1.eof
if Mid(Session("YHQX"),Rs1("QXWS"),1)=1 then
if Mid(Rs("YHZQX"),Rs1("QXWS"),1)=0 then
'Rs("YHZQX")就是需要编辑的用户组的权限,在更上面的语句中进行初始化%>
<option value="<%=Rs1("QXWS")%>"><%=Rs1("CDMC")%></option>
<% end if end if Rs1.movenext loop Rs1.close set Rs1=nothing %>
</select>
<select id="selectqx" size=10 style='width:150'>
<%yhzqx=trim(Rs("YHZQX"))
'Rs("YHZQX")就是需要编辑的用户组的权限,在更上面的语句中进行初始化
for i=1 to len(yhzqx)
if(mid(yhzqx,i,1)="1") then
sql="select CDMC from CDB where QXWS="&i
set Rs1=Conn.execute(sql)
if Not Rs1.eof then%>
<option value="<%=i%>"><%=Rs1("CDMC")%></option>
<%end if Rs1.close set Rs1=nothing end if next Rs1.close set Rs1=nothing %>
</select>
其它操作过程与添加时基本一致。
用户的操作过程与用户组的操作过程基本上也是一样的,所用到的代码也大致相同,不过还是有几个要注意的问题:
第一、添加用户时不需要指定用户的权限,只要指定该用户所属的用户组即可,用户权限自动从用户组继承。当然添加完了也可以对用户组的权限进行修改,为了保障安全,修改的范围也只限当前登陆的用户权限范围内。实现如上所述相同。
第二、用户密码的存贮,建议不要存贮为明码,请加密存贮,在本系统中采取了不可逆转的加密算法,即使知道了加密算法与加密后的结果,也不可能还原为原始密码,有利的保障了安全性。
第三、每当在用户组中添加一个用户时用户组表的用户个数就会增加1,当然删除一个用户也会使用该组的用户个数减1。
第四、用户所属的用户组可以修改,修改后用户的权限与新属用户组的权限一样。
4 安全检查
安全检查指登陆的安全检查与访问每个页面时的安全检查两个方面。
登陆时的安全检查指验证用户名与密码,如果验证通过,将该用户的用户权限存贮于会话变量Session("YHQX")中,以利于随时检验。然后引导入主界面,并根据Session("YHQX")生成系统菜单。
我们重点说一下访问每个页面时的安全检查。大家都知道,与C/S程序不同,某个菜单没有显示出来,就并不代表用户没有办法访问该菜单项的功能,因为用户可以通过直接输入URL访问到该功能模块,所以必须在访问每个页面是也要提供一个权限控制机制,防止用户越权使用。我们用如下代码实现了访问页面时的权限检验:
<% ScriptName=Request.ServerVariables("SCRIPT_NAME")
'取得当次请求的URL
for i=len(ScriptName) to 1 step -1
if Mid(ScriptName,i,1)="/" then exit for
next
ScriptName=Mid(ScriptName,i+1,len(ScriptName)-i)
'分离出请求的ASP页面文件名
sql="select 权限位数 from 菜单权限表 where
相关文件 like '%"&ScriptName&"%'"
set Rs1=Conn.execute(sql)
'查找其属于哪一位权限,根据上面所述的相关文件进行查找
if Rs1.eof and Rs1.bof then
response.write "非法访问!"&ScriptName&"还没有进行注册,请通知系统管理员进行注册!"
Rs1.close
set Rs1=nothing
response.end
end if
'比较当前用户的权限,看是否有访问当前页面的权利
Qxbit=Mid(Session("YHQX"),Rs1(0),1)
if Qxbit="" then
response.write "非法访问!"&"<br>权限位数错误,可能是由于您长时间没有操作而超时退出!<br>请重新登陆或联系管理员!"
Rs1.close
set Rs1=nothing
response.end
end if
if Qxbit="0" then
response.write "非法访问!"&"您没有访问此页的权限!"
Rs1.close
set Rs1=nothing
response.end
end if
Rs1.close
set Rs1=nothing
%>
将该文件存为validate.inc
然后在系统中的每个文件的最顶部用如下语句包含进去:
<!--#include file="validate.inc"-->
这样一来,就实现了页面级的权限检查,并且非常方便。甚至,与一些WEB服务器配合,这个执行过程可以自动完成,并不需要在系统的每一个文件顶部包含validate.inc文件,WEB服务器在调用每一个页面前会自动将validate.inc包含到文件顶部。
在需要开发基于用户的安全策略的B/S应用时,首先将以上所述的框架搭建起来,然后再单独开发各个功能模块,然后用提供的菜单管理功能,将功能模块以搭积木的方式添加进来,这样非常方便,也利于团队的协作开发。由于有开放的菜单与统一的权限控制策略,所开发的应用也能够很好的与用户本身所拥有的B/S应用或者其它厂商的B/S应用集成在一起,为用户提供统一的用户操作界面与权限控制体系,这将使产品非常有竟争力。在我们开发的税务系统后台管理系统中,利用此框架,实现了与用户方和第三方厂商应用的集成。