2013年1月15日火曜日

使用SpringSecurity3实现RBAC权限管理


1、 What? 什么是权限管理?



 
    具体可参见百度:http://baike.baidu.com/view/2108713.htm
    名词备注:
    数据级权限:百科内的权限管理一文解释的比较不错,但其中的“数据级权限”有的人看来会觉得有点摸不着头脑。数据级权限,即表示权限与特定数据有联系的权限,比方说,某用户只能创建100个用户。这个100,就是数据级权限的一个指标。


2、 How?怎么样实现权限管理?

2.1、一种烦恼

也许很多程序员会在权限管理中遇到这样的一个问题。
        大部分项目都需要权限管理系统,但不同的项目背景中,角色的种类和对应的权限灵活多变。往往需要在维护和调研时花费大量的功夫去分析,而最后由于不同客户方不同层面的领导的意见不一或者不同的决策等问题,造成多次的翻工(也许开始你定好了适合他们的权限机制,但后来又有些不可抗拒因素导致你又要修改项目)。
这样的问题是一种无用功,而且是十分让人烦恼的。
如何去解决怎样的问题?

2.2、权限管理架构图




  


2.2.1、用户user


保存基本的用户名,密码,角色表id和用户状态。
管理员可以修改用户的角色。
         PS:系统最基本的状态至少需要保留一个默认管理员账号。

2.2.2、权限privilege


     用来判定(vote)功能及数据级权限管理的依据。
     项目创建者内置的权限集合,不给与管理的权限。否则将可能造成项目功能的缺陷。
      

2.2.3、角色role

   决定用户具体包含权限列表。
      role_privilege连接role与privilege两个表用来表示关系表连接构造many-to-many关系。
同样的,系统默认状态也必须保留至少一种角色为系统自带的管理,这个角色具备系统中全部的权限。也就是说,该角色不受role_ privilege表所限制,会直接读取privilege中的所有权限集。

2.2.4、权限分类privilege_category

为了更加完善的展现权限分配模块,可以构造一个权限分类。

2.3、用SpringSecurity3实现具体功能

    具体SpringSecurity3的配置这里就不详细说明了,可参考网上其他资料。因为本文主要讲述的是如何实现RBAC权限管理模块。
    注:自定义的权限的命名必须以ROLE_ 开头,例如ROLE_USER_CREATE等。

2.3.1、权限与角色误区

使用SpringSecurity3的时候,网上很多的资料都能让你的模块跑起来。但随之而来的是一些误区。
比方说,会把权限和角色两者混淆。比较经典的例子如下:


Java代码  收藏代码
  1. Collection<GrantedAuthority> auths=new ArrayList<GrantedAuthority>();  
  2. GrantedAuthorityImpl roleAdmin=new GrantedAuthorityImpl("ROLE_ADMIN");  
  3. GrantedAuthorityImpl roleUser=new GrantedAuthorityImpl("ROLE_USER");  
  4. auths.add(roleAdmin);  
  5. auths.add(roleUser);<span style="white-space: normal;"> </span>  

然后访问权限的配置如下:
   <intercept-url pattern="/**" access="ROLE_USER" />
虽然也许看上去没什么问题,但其实存在一定的隐患。因为能访问页面的不是因为他是user,而是因为他有“访问”的权限。
    如果后来增加了一个guest的角色,而他能访问系统,但不能含有user的权限。因为user的权限可能有附带很多界面上的功能,但不附上ROLE_USER的话guest又不能访问系统,所以你就不得不修改配置文件中的access。
造成这个的问题的最终原因就是角色和权限混淆了。

2.3.2、权限分配的灵活性

    要想最大限度把权限分配变得灵活,角色提供可摘取权限的功能是必不可少的。而针对不同的项目背景,所有的角色和权限也许会出现各种的变化。但其功能还是离不开分配角色和分配权限。
    而通过role_privilege表,我们可以在用户登录系统的时候,把该用户角色的权限通过SpringSecurity帮助我们放到用户的权限组内。从而我们可以利用SpringSecurity提供的各种标签,标注访问控制等实现权限功能管理和现实。
例子:


Html代码  收藏代码
  1. <sec:authorize ifAnyGranted="ROLE_CREATE,ROLE_UPDATE,ROLE_READ,ROLE_DELETE">  
  2.        <a>用户管理</a>  
  3.  </sec:authorize>  


        即用户拥有OLE_CREATE,ROLE_UPDATE,ROLE_READ,ROLE_DELETE任何一个权限的时候才能查看到用户管理按钮。
点进去之后,我们可以再细分对应的权限操作,没有的权限则该功能模块会不出现。

2.3.3、权限的包含关系

有的时候,权限还必须有“包含”关系,即若你具备了某权限,则另外的权限你也必定会具备。
比方说,有个角色他有删除用户的权限,但他没有读取用户的权限。这样觉得有没有问题呢?
若用户没有读取用户的权限,连列表都不出来,那他如何实现删除?这样看上去虽然不是系统上的问题。但一个完善的系统,必须去考虑这样的情况发生。

2.3.4、自定义UserDetailsService接口实现类

Java代码  收藏代码
  1. import java.util.ArrayList;  
  2. import java.util.Collection;  
  3. import java.util.List;  
  4. import java.util.Set;  
  5.   
  6. import org.apache.commons.collections.CollectionUtils;  
  7. import org.apache.commons.lang.StringUtils;  
  8. import org.springframework.dao.DataAccessException;  
  9. import org.springframework.security.core.GrantedAuthority;  
  10. import org.springframework.security.core.authority.GrantedAuthorityImpl;  
  11. import org.springframework.security.core.userdetails.UserDetails;  
  12. import org.springframework.security.core.userdetails.UserDetailsService;  
  13. import org.springframework.security.core.userdetails.UsernameNotFoundException;  
  14.   
  15. import cn.com.timekey.drugmonitor.business.PrivilegeBus;  
  16. import cn.com.timekey.drugmonitor.business.UserBus;  
  17. import cn.com.timekey.drugmonitor.log.Log;  
  18. import cn.com.timekey.drugmonitor.log.LogFactory;  
  19. import cn.com.timekey.drugmonitor.po.Privilege;  
  20. import cn.com.timekey.drugmonitor.po.Role;  
  21. import cn.com.timekey.drugmonitor.po.RolePrivilege;  
  22. import cn.com.timekey.drugmonitor.po.Users;  
  23.   
  24. /** 
  25.  * @author Kenny 
  26.  */  
  27. public class MyUserDetailsService implements UserDetailsService {  
  28.   
  29.     private static final Log LOGGER = LogFactory  
  30.             .getLog(MyUserDetailsService.class);  
  31.   
  32.     private static final String SYSTEM_ROLE_ID = "1";// 系统默认管理员的id  
  33.   
  34.     private UserBus userBus;  
  35.     private PrivilegeBus privilegeBus;  
  36.   
  37.     public UserDetails loadUserByUsername(String username)  
  38.             throws UsernameNotFoundException, DataAccessException {  
  39.         if (StringUtils.isBlank(username)) {  
  40.             throw new UsernameNotFoundException("no such user.", username);  
  41.         }  
  42.         Users user = userBus.findByName(username);  
  43.         if (user == null) {  
  44.             LOGGER.debug("no such user by " + username);  
  45.             throw new UsernameNotFoundException("no such user.", username);  
  46.         } else if (user.getRole() == null) {  
  47.             LOGGER.debug("no such role by " + username);  
  48.             throw new UsernameNotFoundException("no such user.", username);  
  49.         }  
  50.   
  51.         String adminName = user.getUserName();  
  52.         String password = user.getUserPassword();  
  53.         Role role = user.getRole();  
  54.         @SuppressWarnings("unchecked")  
  55.         Collection<Privilege> privileges = CollectionUtils.EMPTY_COLLECTION;  
  56.         // 判断是否为系统默认管理员,若是,则直接获取privilege表中全部权限。  
  57.         if (StringUtils.equals(role.getRoleId(), SYSTEM_ROLE_ID)) {  
  58.             privileges = privilegeBus.findAll();  
  59.         }  
  60.         Set<RolePrivilege> rolePrivileges = role.getRolePrivileges();  
  61.   
  62.         @SuppressWarnings("unchecked")  
  63.         Collection<GrantedAuthority> authorities = CollectionUtils.EMPTY_COLLECTION;  
  64.         if (privileges.isEmpty() && rolePrivileges != null  
  65.                 && !rolePrivileges.isEmpty()) {  
  66.             privileges = new ArrayList<Privilege>(rolePrivileges.size());  
  67.             for (RolePrivilege rolePrivilege : rolePrivileges) {  
  68.                 privileges.add(rolePrivilege.getPrivilege());  
  69.             }  
  70.         }  
  71.         if (privileges.isEmpty()) {  
  72.             LOGGER.warn("user has not any rolePrivileges.");  
  73.             throw new UsernameNotFoundException("Privilege fail! ", username);  
  74.         }  
  75.         // 构造权限组  
  76.         authorities = generateAuthorities(privileges);  
  77.   
  78.         boolean isEnable = user.getIsActive();// 如果账号有状态的话,可根据查询结果配置该值。  
  79.   
  80.         return new org.springframework.security.core.userdetails.User(  
  81.                 adminName, password, isEnable, truetruetrue, authorities);  
  82.     }  
  83.   
  84.     /** 
  85.      * 构造权限组 
  86.      *  
  87.      * @param rolePrivileges 
  88.      * @return 
  89.      */  
  90.     private Collection<GrantedAuthority> generateAuthorities(  
  91.             Collection<Privilege> privileges) {  
  92.         List<GrantedAuthority> auth = new ArrayList<GrantedAuthority>(  
  93.                 privileges.size());  
  94.         for (Privilege rolePrivilege : privileges) {  
  95.             GrantedAuthority authority = new GrantedAuthorityImpl(  
  96.                     rolePrivilege.getPrivilegeName());  
  97.             auth.add(authority);  
  98.         }  
  99.         return auth;  
  100.     }  
  101.   
  102.     public void setUserBus(UserBus userBus) {  
  103.         this.userBus = userBus;  
  104.     }  
  105.   
  106.     public void setPrivilegeBus(PrivilegeBus privilegeBus) {  
  107.         this.privilegeBus = privilegeBus;  
  108.     }  
  109. }  
  

2.3.5、系统的权限漏洞

    出来页面上使用TAG来实现基本的权限功能隐藏与显示外。还必须注意系统内部的权限判断。

使用拦截器保护限制资源

TAG帮我们隐藏了功能的URL,但该URL还是存在的,只要对方知道URL就能直接发请求过来了,从而绕过了权限管理。
我们可以使用拦截器进一步处理请求权限的问题。具体可以在<http auto-config>代码块中配置,如:

  <intercept-url pattern="/createUser.do" access="ROLE_USER_CREATE" />
Xml代码  收藏代码
  1. <intercept-url pattern="/listUser.do" access="ROLE_USER_READ" />  
  2. <intercept-url pattern="/**" access="ROLE_LOGIN" />  

解释:
访问/createUser.do资源必须有ROLE_USER_CREATE权限。
访问/listUser.do资源必须有ROLE_USER_READ。
访问任何资源都必须有ROLE_LOGIN才行。(也可以用IS_AUTHENTICATED_REMEMBERED)

使用标注保护方法调用

有的时候,程序员在编码中会出现疏忽,导致引用错方法。或者在编码时,没有考虑到权限的问题造成一些跨权限的漏洞。
比方说,某个功能因为疏忽,没在拦截器上配置权限拦截,或者功能定义了两个URL入口,而只有其中一个在拦截器上配置了(一个也许是系统旧的遗留入口)。
这样的情况下就会带来权限漏洞,有不良目的人就可以使用这些漏洞来攻击系统,但最重要的还是造成客户损失。
要更进一步的加强“保险”,我们还可以使用标注在代码里面声明拥有某种权限才能调用特定的方法(也可以使用AOP声明的方式,但个人更加喜欢标注的形式,但标注的话相对于会硬编码些)。
使用标注时,记得要在添加上下面代码才生效喔!
Xml代码  收藏代码
  1. <global-method-security secured-annotations="enabled">  
  2.     </global-method-security>  
  
例子如:


Java代码  收藏代码
  1. import org.springframework.security.access.annotation.Secured;  
  2.   
  3. public interface AccountBusiness {  
  4.   
  5.    @Secured("ROLE_USER_CREATE")  
  6.    public void save(User user);  
  7.   
  8.    @Secured("ROLE_USER_DELETE")  
  9.    public void delete(String id);  
  10.   
  11. }  

3、 Gain 我们的收获

    我们再不用去考虑系统中不同角色的有什么权限的问题了,因为权限分配十分灵活化。程序员不必纠结不同项目中,什么角色应该具备哪些权限而烦恼了,但我们必须配置好一套完善的权限列表来满足用户的分配需求。虽然不完美,但减少了很多的烦恼。
    角色与权限分配的功能由客户或者业务人员自己来决定。我们只要提供好足够满足对方需求的权限范围就可以了。权限缺少的时候,我们可以增加,但这些工作不至于是无用功。

4、 遗留问题

1.用户登录时,必须重新从数据库里面拿角色对应的权限集,资源消耗是否有点多?
    就这个问题而言,我是觉得没必要计较这些资源消耗的,因为权限集再怎么多也不会超过50条吧。
    而权限管理系统,一般并发量也不会大的了。如果真的纠结这样的消耗,也可以放用static map用来实现角色与权限集的获取,但记得用上观察者模式。因为权限集是可以被修改的,不用观察者的话就会出现得到过期的权限集了。

2.虽然考虑到数据级的权限管理问题,但目前还是没有提供这样的案例。

3.Group用户组还不在此架构范围内。

0 件のコメント:

コメントを投稿