20.1 场景问题
20.1.1 加入权限控制
考虑这样一个问题,给系统加入权限控制,这基本上是所有的应用系统都有的功能了。
对于应用系统而言,一般先要登录系统,才可以使用系统的功能,登录过后,用户的每次操作都需要经过权限系统的控制,确保该用户有操作该功能的权限,同时还要控制该用户对数据的访问权限、修改权限等等。总之一句话,一个安全的系统,需要对用户的每一次操作都要做权限检测,包括功能和数据,以确保只有获得相应授权的人,才能执行相应的功能,操作相应的数据。
举个例子来说吧:普通人员都有能查看到本部门人员列表的权限,但是在人员列表中每个人员的薪资数据,普通人员是不可以看到的;而部门经理在查看本部门人员列表的时候,就可以看到每个人员相应的薪资数据。
现在就要来实现为系统加入权限控制的功能,该怎么实现呢?
为了让大家更好的理解后面讲述的知识,先介绍一点权限系统的基础知识。几乎所有的权限系统都分成两个部分,一个是授权部分,一个是验证部分,为了理解它们,首先解释两个基本的名词:安全实体和权限。
- 安全实体:就是被权限系统检测的对象,比如工资数据。
- 权限:就是需要被校验的权限对象,比如查看、修改等。
安全实体和权限通常要一起描述才有意义,比如有这么个描述:“现在要检测登录人员对工资数据是否有查看的权限”, “工资数据”这个安全实体和“查看”这个权限一定要一起描述。如果只出现安全实体描述,那就变成这样:“现在要检测登录人员对工资数据”,对工资数据干什么呀,没有后半截,一看就知道不完整;当然只有权限描述也不行,那就变成:“现在要检测登录人员是否有查看的权限”,对谁的查看权限啊,也不完整。所以安全实体和权限通常要一起描述。
了解了上面两个名词,来看看什么是授权和验证:
- 所谓授权是指:把对某些安全实体的某些权限分配给某些人员的过程。
- 所谓验证是指:判断某个人员对某个安全实体是否拥有某个或某些权限的过程。
也就是说,授权过程即是权限的分配过程,而验证过程则是权限的匹配过程。在目前应用系统的开发中,多数是利用数据库来存放授权过程产生的数据,也就是说:授权是向数据库里面添加数据、或是维护数据的过程,而匹配过程就变成了从数据库中获取相应数据进行匹配的过程了。
为了让问题相对简化一点,就不去考虑权限的另外两个特征,一个是继承性,一个是最近匹配原则,都什么意思呢,还是解释一下:
- 权限的继承性指的是:如果多个安全实体存在包含关系,而某个安全实体没有相应的权限限制,那么它会继承包含它的安全实体的相应权限。
比如:某个大楼和楼内的房间都是安全实体,很明显大楼这个安全实体会包含楼内的房间这些安全实体,可以认为大楼是楼内房间的父级实体。现在来考虑一个具体的权限——进入某个房间的权限。如果这个房间没有门,也就是谁都可以进入,相当于这个房间对应的安全实体,没有进入房间的权限限制,那么是不是说所有的人都可以进入这个房间呢?当然不是,某人能进入这个房间的前提是:这个人要有权限进入这个大楼,也就是说,这个时候房间这个安全实体,它本身没有进入权限的限制,但是它会继承父级安全实体的进入权限。 - 权限的最近匹配原则指的是:如果多个安全实体存在包含关系,而某个安全实体没有相应的权限限制,那么它会向上寻找并匹配相应权限限制,直到找到一个离这个安全实体最近的拥有相应权限限制的安全实体为止。如果把整个层次结构都寻找完了都没有匹配到相应权限限制的话,那就说明所有人对这个安全实体都拥有这个相应的权限限制。
继续上面权限继承性的例子,如果现在这个大楼是坐落在某个机关大院内,这就演变成了,要进入某个房间,首先要有进入大楼的权限,要进入大楼又需要有能进入机关大院的权限。
所谓最近匹配原则就是,如果某个房间没有门,也就意味着这个房间没有进入的权限限制,那么它就会向上继续寻找并匹配,看看大楼有没有进入的权限限制,如果有就使用这个权限限制,终止寻找;如果没有,继续向上寻找,直到找到一个匹配的为止。如果最后大院也没有进入的权限限制,那就变成所有人都可以进入到这个房间里面来了。
20.1.2 不使用模式的解决方案
1:看看现在都已经有什么了
系统的授权工作已经完成,授权数据记录在数据库里面,具体的数据结构就不去展开了,反正里面记录了人员对安全实体所拥有的权限。假如现在系统中已有如下的授权数据:
张三 对 人员列表 拥有 查看的权限 李四 对 人员列表 拥有 查看的权限 李四 对 薪资数据 拥有 查看的权限 李四 对 薪资数据 拥有 修改的权限 |
2:思路选择
由于操作人员进行授权操作过后,各人员被授予的权限是记录在数据库中的,刚开始有开发人员提出,每次用户操作系统的时候,都直接到数据库里面去动态查询,以判断该人员是否拥有相应的权限,但很快就被否决掉了,试想一下,用户操作那么频繁,每次都到数据库里面动态查询,这会严重加剧数据库服务器的负担,使系统变慢。
为了加快系统运行的速度,开发小组决定采用一定的缓存,当每个人员登录的时候,就把该人员能操作的权限获取到,存储在内存中,这样每次操作的时候,就直接在内存里面进行权限的校验,速度会大大加快,这是典型的以空间换时间的做法。
3:实现示例
(1)首先定义描述授权数据的数据对象,示例代码如下:
/** * 描述授权数据的数据model */ public class AuthorizationModel { /** * 人员 */ private String user; /** * 安全实体 */ private String securityEntity; /** * 权限 */ private String permit; public String getUser() { return user; } public void setUser(String user) { this.user = user; } public String getSecurityEntity() { return securityEntity; } public void setSecurityEntity(String securityEntity) { this.securityEntity = securityEntity; } public String getPermit() { return permit; } public void setPermit(String permit) { this.permit = permit; } } |
(2)为了测试方便,做一个模拟的内存数据库,把授权数据存储在里面,用最简单的字符串存储的方式。示例代码如下:
/** * 供测试用,在内存中模拟数据库中的值 */ public class TestDB { /** * 用来存放授权数据的值 */ public static Collection<String> colDB = new ArrayList<String>(); static{ //通过静态块来填充模拟的数据 colDB.add("张三,人员列表,查看"); colDB.add("李四,人员列表,查看"); colDB.add("李四,薪资数据,查看"); colDB.add("李四,薪资数据,修改"); //增加更多的授权数据 for(int i=0;i<3;i++){ colDB.add("张三"+i+",人员列表,查看"); } } } |
(3)接下来实现登录和权限控制的业务,示例代码如下:
/** * 安全管理,实现成单例 */ public class SecurityMgr { private static SecurityMgr securityMgr = new SecurityMgr(); private SecurityMgr(){ } public static SecurityMgr getInstance(){ return securityMgr; } /** * 在运行期间,用来存放登录人员对应的权限, * 在Web应用中,这些数据通常会存放到session中 */ private Map<String,Collection<AuthorizationModel>> map = new HashMap<String,Collection<AuthorizationModel>>();
/** * 模拟登录的功能 * @param user 登录的用户 */ public void login(String user){ //登录时就需要把该用户所拥有的权限,从数据库中取出来,放到缓存中去 Collection<AuthorizationModel> col = queryByUser(user); map.put(user, col); } /** * 判断某用户对某个安全实体是否拥有某权限 * @param user 被检测权限的用户 * @param securityEntity 安全实体 * @param permit 权限 * @return true表示拥有相应权限,false表示没有相应权限 */ public boolean hasPermit(String user,String securityEntity ,String permit){ Collection<AuthorizationModel> col = map.get(user); if(col==null || col.size()==0){ System.out.println(user+"没有登录或是没有被分配任何权限"); return false; } for(AuthorizationModel am : col){ //输出当前实例,看看是否同一个实例对象 System.out.println("am=="+am); if(am.getSecurityEntity().equals(securityEntity) && am.getPermit().equals(permit)){ return true; } } return false; } /** * 从数据库中获取某人所拥有的权限 * @param user 需要获取所拥有的权限的人员 * @return 某人所拥有的权限 */ private Collection<AuthorizationModel> queryByUser( String user){ Collection<AuthorizationModel> col = new ArrayList<AuthorizationModel>(); for(String s : TestDB.colDB){ String ss[] = s.split(","); if(ss[0].equals(user)){ AuthorizationModel am = new AuthorizationModel(); am.setUser(ss[0]); am.setSecurityEntity(ss[1]); am.setPermit(ss[2]);
col.add(am); } } return col; } } |
(4)好不好用呢,写个客户端来测试一下,示例代码如下:
public class Client { public static void main(String[] args) { //需要先登录,然后再判断是否有权限 SecurityMgr mgr = SecurityMgr.getInstance(); mgr.login("张三"); mgr.login("李四"); boolean f1 = mgr.hasPermit("张三","薪资数据","查看"); boolean f2 = mgr.hasPermit("李四","薪资数据","查看");
System.out.println("f1=="+f1); System.out.println("f2=="+f2); for(int i=0;i<3;i++){ mgr.login("张三"+i); mgr.hasPermit("张三"+i,"薪资数据","查看"); } } } |
运行结果如下:
am==cn.javass.dp.flyweight.example1.AuthorizationModel@1eed786 am==cn.javass.dp.flyweight.example1.AuthorizationModel@187aeca am==cn.javass.dp.flyweight.example1.AuthorizationModel@e48e1b f1==false f2==true am==cn.javass.dp.flyweight.example1.AuthorizationModel@12dacd1 am==cn.javass.dp.flyweight.example1.AuthorizationModel@119298d am==cn.javass.dp.flyweight.example1.AuthorizationModel@f72617 |
输出结果中的f1为false,表示张三对薪资数据没有查看的权限;而f2为true,表示李四对对薪资数据有查看的权限,是正确的,基本完成了功能。
20.1.3 有何问题
看了上面的实现,很简单,而且还考虑了性能的问题,在内存中缓存了每个人相应的权限数据,使得每次判断权限的时候,速度大大加快,实现得挺不错,难道有什么问题吗?
仔细想想,问题就来了,既有缓存这种方式固有的问题,也有我们自己实现上的问题。先说说缓存固有的问题吧,这个不在本次讨论之列,大家了解一下。
- 缓存时间长度的问题,就是这些数据应该被缓存多久,如果是Web应用,这种跟登录人员相关的权限数据,多是放在session中进行缓存,这样session超时的时候,就会被清除掉。如果不是Web应用呢?就得自己来控制了,另外就算是在Web应用中,也不一定非要缓存到session超时才清除。总之,控制缓存数据应该被缓存多长时间,是实现高效缓存的一个问题点。
- 缓存数据和真实数据的同步问题,这里的同步是指的数据同步,不是多线程的同步。比如:上面的授权数据是存放在数据库里的,运行的时候缓存到内存里面,如果真实的授权数据在运行期间发生了改变,那么缓存里的数据就应该和数据库的数据同步,以保持一致,否则数据就错了。如何合理的同步数据,也是实现高效缓存的一个问题点。
- 缓存的多线程并发控制,对于缓存的数据,有些操作从里面取值,有些操作向缓存里面添加值,有些操作在清除过期的缓存数据,有些操作在进行缓存和真实数据的同步,在一个多线程的环境下,如何合理的对缓存进行并发控制,也是实现高效缓存的一个问题点。
先简单提这么几个,事实上,实现合理、高效的缓存也不是一件很轻松的事情,好在这些问题,都不在我们这次的讨论之列,这里的重心还是来讲述模式,而不是缓存实现。
再来看看前面实现上的问题,仔细观察在上面输出结果中框住的部分,这些值是输出对象实例得到的,默认输出的是对象的hashCode值,而默认的hashCode值可以用来判断是不是同一对象实例。在Java中,默认的equals方法比较的是内存地址,而equals方法和hashCode方法的关系是:equals方法返回true的话,那么这两个对象实例的hashCode必须相同;而hashCode相同,equals方法并不一定返回true,也就是说两个对象实例不一定是同一对象实例。换句话说,如果hashCode不同的话,铁定不是同一个对象实例。
仔细看看上面输出结果,框住部分的值是不同的,表明这些对象实例肯定不是同一个对象实例,而是多个对象实例。这就引出一个问题了,就是对象实例数目太多,为什么这么说呢?看看就描述这么几条数据,数数看有多少个对象实例呢?目前是一条数据就有一个对象实例,这很恐怖,数据库的数据量是很大的,如果有几万条,几十万条,岂不是需要几万个,甚至几十万个对象实例,这会耗费掉大量的内存。
另外,这些对象的粒度都很小,都是简单的描述某一个方面的对象,而且很多数据是重复的,在这些大量重复的数据上耗费掉了很多的内存。比如在前面示例的数据中就会发现有重复的部分,见下面框住的部分:
张三 对 人员列表 拥有 查看的权限 李四 对 人员列表 拥有 查看的权限 李四 对 薪资数据 拥有 查看的权限 李四 对 薪资数据 拥有 修改的权限 |
前面讲过,对于安全实体和权限一般要联合描述,因此对于“人员列表 这个安全实体 的 查看权限 限制”,就算是授权给不同的人员,这个描述是一样的。假设在某极端情况下,要把“人员列表 这个安全实体 的 查看权限 限制”授权给一万个人,那么数据库里面会有一万条记录,按照前面的实现方式,会有一万个对象实例,而这些实例里面,有大部分的数据是重复的,而且会重复一万次,你觉得这是不是个很大的问题呢?
把上面的问题描述出来就是:在系统当中,存在大量的细粒度对象,而且存在大量的重复数据,严重耗费内存,如何解决?
ps:转载私塾在线