写在前面:
发表之前,特意google了一下,没有找到类似主题,因此,请尊重一下作者的劳动成果,如需转载,请注明作者。
spring security 框架的核心是通过建立用户->角色->资源三者的多对多关系来管理用户权限,它的配置非常灵活,但正是这种灵活性给框架的使用者带来了诸多困惑,倒底是该给单一用户分配更多角色还是该把更多资源分配给某个特定角色呢,没有一定之规,我的做法是给单一用户分配单一角色,单一角色分配单一(种)资源,将权限模型简化为三者的近似一对一关系,然后利用角色继承,构造角色的层级关系,从而实现用户权限的扩展,这样一来,表记录将可以大大简化,维护也更为简便。用户登录上来,表现层上只有他的原始身份,不会有类似[ROLE_ADMIN,ROLE_USER]这样的复合身份,更加符合现实世界的需求。不过,虽然框架本身支持角色继承,但其实现方式略显怪异,需要在XML中以类似下面的格式来配置:
<beans:bean id="roleHierarchy" class="org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl"> <beans:property name="hierarchy"> <beans:value> ROLE_BOSS > ROLE_ADMIN ROLE_ADMIN > ROLE_SYSTEM_MANAGER ROLE_SYSTEM_MANAGER > ROLE_DISTRICT_1_MANAGER ROLE_SYSTEM_MANAGER > ROLE_DISTRICT_2_MANAGER ROLE_DISTRICT_1_MANAGER > ROLE_USER ROLE_DISTRICT_2_MANAGER > ROLE_USER </beans:value> </beans:property> </beans:bean>
RoleHierarchyImpl实例对象中有一个方法getReachableGrantedAuthorities,读取这段文本,装配成权限,叠加进原始权限,供RoleVoter对象调用。在2.x中还可以使用数据库表来配置,但其用到的表结构也相当简陋,一个包含字段,一个被包含字段,看起来跟上面这段配置没有两样,这样的表在实际应用中几乎没有什么用,还不如上面这段配置来得简便。
仔细看这段配置,ROLE_SYSTEM_MANAGER继承了ROLE_DISTRICT_1_MANAGER的权限,同时它还继承了ROLE_DISTRICT_2_MANAGER的权限,这需要配置两条,而ROLE_DISTRICT_1_MANAGER和ROLE_DISTRICT_2_MANAGER位于同一级别,都继承了ROLE_USER的权限,还得配置两条,因此,一旦同级角色的数量增加,配置的条目数量将成倍增加,这实在不是一件令人高兴的事。况且,将继承关系写死在配置文件中,失去了灵活性,无法对继承关系进行动态管理,同样让人颇感不爽。
看来需要扩展框架的RoleHierarchy实现,初步想法很自然的第一步,就是在数据库的ROLE表中增加表示层级关系的LEVEL字段,让拥有高权限的角色包含低权限角色,然而这还不够,要体现灵活性,还要增加一个表示可继承与否的INHERITABLE字段,对继承关系进行约束,修改后的ROLE表结构是这样的:
Field | Type | Null | Key | Default | Extra |
id | int(11) | NO | PRI | NULL | auto_increment |
description | varchar(255) | YES | NULL | ||
name | varchar(255) | YES | NULL | ||
inheritable | bit(1) | YES | NULL | ||
level | int(11) | YES | NULL |
接下来要颇费一番周折,RoleHierarchyImpl实现了RoleHierarchy接口,这个接口里只有一个方法getReachableGrantedAuthorities,需要把原始权限与ROLE表中的LEVEL关系进行比对,然后把符合条件的权限装配进去,麻烦就在于比对逻辑,为了提升效率,我先后写了两个实现,下面是最终的实现代码:
package com.hony.prj.security; import java.util.ArrayList; import java.util.Collection; import java.util.List; import javax.annotation.PostConstruct; import javax.annotation.Resource; import org.springframework.orm.hibernate3.HibernateTemplate; import org.springframework.security.access.hierarchicalroles.RoleHierarchy; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.authority.GrantedAuthorityImpl; import org.springframework.stereotype.Service; @Service("roleHierarchy") public class MyRoleHierarchyImpl implements RoleHierarchy { @Resource private HibernateTemplate hibernateTemplate; private List<GrantedAuthority> appAuthorities; private List<Boolean> appInheritables; private List<Integer> appLevels; @PostConstruct public void loadAppRoleAttrs() { List<Object[]> appRoleAttrs=hibernateTemplate.find( "select r.name,r.inheritable,r.level from Role r order by r.level asc"); if(appRoleAttrs!=null){ appAuthorities=new ArrayList<GrantedAuthority>(); appInheritables=new ArrayList<Boolean>(); appLevels=new ArrayList<Integer>(); for(Object[] ra : appRoleAttrs){ appAuthorities.add(new GrantedAuthorityImpl((String)ra[0])); appInheritables.add((Boolean)ra[1]); appLevels.add((Integer) ra[2]); } } } @Override public Collection<GrantedAuthority> getReachableGrantedAuthorities( Collection<GrantedAuthority> authorities) { if (authorities == null || authorities.isEmpty()) { return AuthorityUtils.NO_AUTHORITIES; } List<GrantedAuthority> reachableGrantedAuthorities=new ArrayList<GrantedAuthority>(); List<GrantedAuthority> currentAuthorities = new ArrayList<GrantedAuthority>(); currentAuthorities.addAll(authorities); reachableGrantedAuthorities.addAll(currentAuthorities); if(currentAuthorities.get(0).getAuthority().equals("ROLE_ANONYMOUS")){ return reachableGrantedAuthorities; } int aSize=appAuthorities.size(); int cSize=currentAuthorities.size(); String appCurrAuthName=null; String currAuthName=null; int i=0,j=0; for(;i<aSize;i++){ appCurrAuthName=appAuthorities.get(i).getAuthority(); currAuthName=currentAuthorities.get(0).getAuthority(); if(appCurrAuthName.equals(currAuthName)){ break; } } if(i==aSize-1){ return reachableGrantedAuthorities; } int currLevel=appLevels.get(i); for(i++,j++;i<aSize;i++){ appCurrAuthName=appAuthorities.get(i).getAuthority(); boolean appCurrInheritable=appInheritables.get(i); currAuthName=(j<cSize) ? currentAuthorities.get(j).getAuthority() : null; if(appCurrInheritable){ if(appCurrAuthName.equals(currAuthName)){ j++; } else{ if(currLevel!=appLevels.get(i)){ reachableGrantedAuthorities.add(appAuthorities.get(i)); } } } } for(GrantedAuthority authority : reachableGrantedAuthorities){ System.out.println("reachableGrantedAuthority is: "+authority.getAuthority()); } return reachableGrantedAuthorities; } }
由于采用Annotation方式注入Bean对象,配置文件applicationContext-security.xml只需要简单地配置一下:
<beans:bean id="roleHierarchyVoter" class="org.springframework.security.access.vote.RoleHierarchyVoter">
<beans:constructor-arg ref="roleHierarchy" />
</beans:bean>
<beans:bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<beans:property name="allowIfAllAbstainDecisions" value="false"/>
<beans:property name="decisionVoters">
<beans:list>
<beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
<beans:ref bean="roleHierarchyVoter" />
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
</beans:list>
</beans:property>
</beans:bean>
解释一下代码:
@PostConstruct public void loadAppRoleAttrs() { List<Object[]> appRoleAttrs=hibernateTemplate.find( "select r.name,r.inheritable,r.level from Role r order by r.level asc"); if(appRoleAttrs!=null){ appAuthorities=new ArrayList<GrantedAuthority>(); appInheritables=new ArrayList<Boolean>(); appLevels=new ArrayList<Integer>(); for(Object[] ra : appRoleAttrs){ appAuthorities.add(new GrantedAuthorityImpl((String)ra[0])); appInheritables.add((Boolean)ra[1]); appLevels.add((Integer) ra[2]); } } }
Bean初始化时加载进Role权限,给三个成员变量赋值,这样只需加载一次即可,以后权限数据更新后也可以调这个方法更新实例中的成员变量,用到了PostConstruct注解。
if(currentAuthorities.get(0).getAuthority().equals("ROLE_ANONYMOUS")){ return reachableGrantedAuthorities; }
如果配置文件中有类似于<intercept-url pattern="/index.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY"/>这样的条目,需要特别处理权限为“ROLE_ANONYMOUS”的情况。
for(;i<aSize;i++){ appCurrAuthName=appAuthorities.get(i).getAuthority(); currAuthName=currentAuthorities.get(0).getAuthority(); if(appCurrAuthName.equals(currAuthName)){ break; } } if(i==aSize-1){ return reachableGrantedAuthorities; }
查找现有权限在所有权限中起始位置(按LEVEL排序后),如果排在最后,就不用继续了。
int currLevel=appLevels.get(i); for(i++,j++;i<aSize;i++){ appCurrAuthName=appAuthorities.get(i).getAuthority(); boolean appCurrInheritable=appInheritables.get(i); currAuthName=(j<cSize) ? currentAuthorities.get(j).getAuthority() : null; if(appCurrInheritable){ if(appCurrAuthName.equals(currAuthName)){ j++; } else{ if(currLevel!=appLevels.get(i)){ reachableGrantedAuthorities.add(appAuthorities.get(i)); } } } }
找到起始位置后,要把它的LEVEL值取出来,以避免同级包含。
其他要注意的是,我定义的排序逻辑是按升序排列,LEVEL值越小,权限越大,USER表对应的POJO类也要作适当修改:
User.class
@Override
public Collection<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorities=new ArrayList<GrantedAuthority>(roles.size());
Collections.sort(roles, new RoleByLevelComparator(SortType.ASC));
for (Role role : roles){
grantedAuthorities.add(new GrantedAuthorityImpl(role.getName()));
}
return grantedAuthorities;
}
Collections.sort(roles, new RoleByLevelComparator(SortType.ASC))确保在返回权限集合前按Level属性升序排列,虽然我自己用单一角色方案,但并不排斥框架允许的多角色方案,事先排序是完全有必要的。
大体上就是这些,如有疑问,欢迎交流。