我在通用权限的设计(上)一文中很多问题并没有诉说详细,而且估计我写“通用”两字的时候激发了很多人的不满,我这里先道个歉。
从解决方案的角度出发,没有绝对通用的权限设计,这点我也赞同,只能针对项目的实际需求来开发对应的权限模块,这点是肯定的。我这里只是记录一种做法而已,而且这种做法必然有其适用的范围,而且如果把企业或者公司部门的因素也考虑进去,我觉得反而难以把做法说清,所以选择用最简单的设计来说明问题。实际情况下肯定要针对简单设计来做扩展的。

上篇的图这里再插入一遍:


我简单的说明一下,Application为功能模块表,他实际上没有什么具体作用,只是用来约束功能点的归属而已。Permission才是核心的功能点表,字段有最简单的功能名以及其他的字段,其中最重要的是功能点对应的权值(上篇说到的二进制编码),实际上这里没必要用0010这样的方式来保存,如果你的系统很小,功能点很少,那么可以用int类型来保存,权值只要满足2的n-1次方即可,至于原因是什么,我这里就没必要做说明了。那么以后业务扩充的功能点,只要满足这个权值规律就行了。如果系统的功能点再多点,可以考虑用bigint来保存,如果再大,可以考虑更大的类型来保存,甚至用字符来保存都可以,至于代码的接收,同样的需要有对应的类型来保存,小的不用说,大的可以参考http://pavelsem.blogspot.com/2008/04/big-number-calculations-in-c.html。针对这个功能表,我们可以做一个针对功能点的增删改操作,每次按照权值规律来赋值新的功能点,当然如果你比较懒,每次都在数据库里面加也可以,但是当数值较大的时候,每次估计都得拿个计算机来按了。所以大家大可估算一下这种做法的适用范围,以及当权值很大的时候,这种做法与传统做法哪中更好。

讲完功能点,应该来讲一下如何创建角色,为什么角色与功能点之间完全没有关联,实际上很简单,我在上篇中已经把这个道理说明了,如果我们的功能点仍然有增加,删除,修改,查看,对应的权值分别是1,2,4,8,那么当我们创建一个角色B,要求角色B拥有这四个功能点的时候只需要进行如下操作:1 + 2 + 4 + 8 =15 即可,把15这个值以及新建的角色信息存入角色表即可。那么当用户登陆的时候,只需要从用户角色对应表当中找到对应的角色,并把角色的权值做计算即可,假如又有角色C的权值为1,而用户A同时具备角色B以及角色C,那么当用户登陆的时候只要根据关联表找到他对应的两个角色,再把角色B以及角色C的权值进行按位或操作,即:1 | 15 = 15,15这个值就是用户A所具有的权值了,我们把这个权值保存起来,放入Session或者Cookies当中,显然15这个权值与1,2,4,8做按位与操作的时候皆可得到1,2,4,8,于是用户A就得到了角色B以及角色C的权限,并且角色B跟角色C的权限已经进行了合并。至此,用户登陆完之后就与数据库脱离了关系,所有权限的判断只需要拿具体功能点的权值与用户的权值进行按位与操作即可,不用再查表或者遍历功能点集合。至于角色的更新,只需要把新计算出来的权值更新到对应的角色即可。

针对实际情况,我们不可能在判断某个用户是否有某个功能点的时候都拿个数来与当前用户的权值做比较,更多是把它抽象为一个方法:

if (roleHandle.CheckUserPer("CreateTipType"))
{
    
//..
}


如上所示,CreateTipType对应的是一个业务的功能点名称,他对应一个权值,以上所说的过程全部集成在一个方法里面判断就可以了,当然你要用什么方式来做你的功能点与权值的对应都可以,并不一定要用名称,也可以用约定好的数字甚至其他表达方式都可以,但是绝对比你直接传个数字来得直观。
采用这种做法的时候,还会碰到很多具体的问题,诸如菜单的生成,权值的配置,角色的配置,角色的更新,用户权值的更新与保存等实际问题,我这里就不做一一说明了,感兴趣的朋友我们可以一起讨论。

posted @ 2009-01-09 16:07 b4n73 阅读(2262) 评论(14) 编辑

讲权限模块的设计,其实没什么太大的意义,园子里面很多高人已经写过了。前段时间自己写了一个权限模块,现在跟大家分享一下做法。
讲我的做法之前,先要说说比较通用的权限模块是如何设计的,通常是一张用户表,角色表,用户与角色关联表,功能表,角色与功能关联表,模块表。
如上设计之后,当要判断一个用户是否具有某个权限的时候先要从用户与角色关联表出发,找到当前用户所属的角色,然后再去角色功能关联表里面查找用户所属角色所具有的功能点。查找出来的用户具有功能点是一个集合,还要把当前的功能点与集合里面的所有功能点比较,如果在集合里面找到了功能点,就证明此用户具备该功能点的权限,否则就证明该用户不具备此功能点的权限。
以上做法看似繁琐,但是大部分的操作都是在用户登陆的时候就把其对应的功能点集合取出,放入Session中,那么以后判断用户是否具备某个功能点的时候只需要把功能点与Session当中的功能点集合遍历比较即可。虽然极大的避免了查库,但是还有没有一种更好的做法能取代这种功能点遍历查找,让权限的判断与集合完全无关呢?

我们先举个例子,这里用二进制的方式来表示:
假如有如下功能点以及对应的二进制码

增加——0001
删除——0010
修改——0100
查找——1000

如上定义之后,我们假设一个用户A具有增加以及删除的权限,那么我们把增加以及删除操作的二进制码进行按位或操作:0001 | 0010 = 0011
我们把0011赋给用户A,那么当我们要判断此用户是否具备增加操作的时候,只需要把增加操作的二进制码与赋给用户A的二进制码进行按位与操作:0001 & 0011 = 0001
你是否已经看出点端倪了,只要计算的结果等于要判断的功能点的二进制码,就证明用户A具有该功能点的权限了。这样的话,判断某个用户是否具备某个功能点的权限的时候只需要把用户的二进制码与功能点的二进制码进行按位与操作即可。每次判断都非常简单。

下面放出我的设计:

从上图可以看到模块表与功能表有关联,但是功能点与角色表不再有关联,因为当创建角色之后,权值(之前所说的二进制码)就已经计算好并且加入到角色表当中,从此与后面的模块表与功能表脱节无关。用户与角色有关联,是为了用户与角色可以是多对多的关系,一个用户可以具备多个角色的权限,一个角色可以对应多个用户。
这次就先说到这里,下篇再详细说说创建角色,更新角色,创建用户等更多具体操作如何实现。

posted @ 2009-01-09 11:08 b4n73 阅读(3049) 评论(33) 编辑
在讲MVC的token的时候,我简单的说了一下如果用LINQ生成实体的话如何做业务逻辑验证。现在我来详细说一下:

        [Column(Storage="_userMail", DbType="VarChar(100)")]
        
public string userMail
        {
            
get
            {
                
return this._userMail;
            }
            
set
            {
                
if ((this._userMail != value))
                {
                    
this.OnuserMailChanging(value);
                    
this.SendPropertyChanging();
                    
this._userMail = value;
                    
this.SendPropertyChanged("userMail");
                    
this.OnuserMailChanged();
                }
            }
        }


上面代码是dbml的designer类,是由LINQ自动生成的代码,从代码中可以看到LINQ为userMail的字段生成了get,set方法,留意一下set方法里面,首先调用了this.OnuserMailChanging(value)的方法,也就是说我们可以在这个方法里面做一些业务逻辑的判断,如:

       partial void OnuserMailChanging(string value)
        {
            Regex reg 
= new Regex(@"\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*");


            
if (!reg.IsMatch(value))
            {
                
throw new ArgumentException("format error!");
            }
        }

我在OnuserMailChanging方法中去判断传入的Value是否是邮件格式,这个方法会在实体赋值的时候(set)的时候就调用,如果格式错误,则报错,不再进行赋值,如果我们试图在Changing方法中就对value进行过滤HTML字符的操作,那么实际上是无效的,首先是形参跟实参的关系,改了value的值实际上不会更改到实参的值,其次如果在Changing方法里面就更改_userMail字段的值,如下所示:

        partial void OnuserMailChanging(string value)
        {
            _userMail 
= FilterContent(_userMail);
        }

在Changing事件中过滤并且赋值给字段,这么改实际上是可行的,但是留意下desinger类里面的set处理中当调用完Changing事件之后又会把value值重新赋给字段,那么之前Changing事件里面所做的赋值操作实际上是没有意义的。有人说可以把后面的赋值代码去掉,实际上desinger类不主张修改,因为该类是自动生成的,你所做的修改实际上下次更改layout的时候又会被覆盖掉了。最好的做法是把过滤放到Changed事件里面,留意下set处理中Changed事件是在赋值后,所以在里面更改字段的值是有意义的,如:

        partial void OnuserMailChanged()
        {
            _userMail 
= FilterContent(_userMail);
        }

这样把Changing与Changed相结合,在实体中即做格式验证,又做脚本过滤,才能使实体更加健壮!
posted @ 2009-01-04 16:16 b4n73 阅读(741) 评论(0) 编辑

Ruby中有提供表单的令牌token,struts中也有提供token
今天为了增加表单提交的安全性,于是也想着在mvc里面模拟一个类似token的东西。
做法很简单,写两个filter就可以了,第一个用来产生token,并且将这个token存入Session或者Cookies中,这个filter在action产生前触发,于是生成的页面就可以在表单里面写一个hidden域来存放这个生成的token;另外还要再写一个filter来验证表单提交的时候hidden域中的token跟服务器端保存的token是否一致,这里可以在filter里面重写两个事件,一个在action触发之前判断,一个在action之后,让这个token失效或者重置。

自己实现后再翻查了一下资料,原来ASP.NET MVC从PV5开始就提供了类似这样的token:

先在提交页的表单中写
<%=Html.AntiForgeryToken() %> 
这样就会在客户端页面上生成一个类似

Code


的隐藏域。然后只需要在action头部加上一个系统提供的过滤器,就可以达到目的了,如:

        /// <summary>
        
/// /Home/Login 登陆
        
/// </summary>
        [Microsoft.Web.Mvc.ValidateAntiForgeryToken]
        
public void Login()

其实这种令牌形式只是让安全性稍稍提高了一下,如果别人要外部提交表单,实际上这种方式照样可以被拦截下来。所有Request信息都是可以伪造的,所以最好的方法还是增加底层的安全性,如果用Linq to SQL生成实体的话,那么在model那里实际上就可以做过滤了,如:

    partial class tips : ModelFilter
    {

        
partial void OntitleChanged
        {
            _title 
= FilterContent(_title);
            
if (_title == string.Empty)
            {
                
throw new Exception("not null!");
            }
        }

        
partial void OncontentChanged()
        {
            _content
= FilterJS(_content);
            
if (_content == string.Empty)
            {
                
throw new Exception("not null!");
            }
        }
    }

有一个tips的实体,只需要写一个过滤的基类,然后tips实体继承这个基类就可以了,上面只是简单的把title过滤掉HTML标签,把content过滤掉JS脚本,过滤一般是在Changed事件,如果放在changing事件中,赋值的时候又会把value重新赋给实体了。你也可以根据你的字段来做过滤,如写邮件格式或者电话号码格式的验证,这些格式验证的话就可以写在changing事件。前台验证并不能避免外部提交表单的问题,如果业务不过滤,实体不过滤,很容易就被别人攻击了。很多人主张写一个工具类在业务层那里过滤,需要过滤的地方就调用,使用什么方法都好,因人而异,我个人觉得把实体写完善了,才能减少工作量,因为说不准哪天你就忘记调用你的工具类去过滤了,回过头去找,很累。开blog第一篇,先写到这了。
posted @ 2009-01-03 10:46 b4n73 阅读(243) 评论(1) 编辑