Acegi授权策略和保护web资源

来源:互联网 发布:js选中input内容 编辑:程序博客网 时间:2024/05/16 09:31

分类: Java

    在通过各种认证途径获得Authentication认证对象后,事情的发展并没有结束。用户认证只是确定当前用户是否存在,而且她的身份也被辨认出来。至于已经认证的用户是否有权限操控目标资源(web资源,业务方法,领域对象),那么就还需要经过用户授权的考验!
    下面就总体上给出acegi是如何实施用户授权工作的,以及围绕web资源的授权操作展开!
    当acegi完成用户的认证操作时,认证结果会存储到Authentication对象中。针对不同的场合,Acegi内置了多种Authentication对象类型。比如:PrincipalAcegiUserToken,UsernamePasswordAuthenticationToken,X509AuthenticationToken等,他们必须实现Authentication接口!
    那么acegi是如何实施用户授权的呢?当已经认证的用户视图通过手中的校色集合操控目标资源时,acegi内置的AOP拦截器会采用公平投票机制评估这一举动,实际上公平投票贯穿于目标资源的调用前后,调用前,AOP拦截器确保当前用户确实有要求的角色集合;调用后,AOP拦截器会审核操作结果,比如从返回的领域对象集合中删除当前用户不能够操控的那些领域对象!
   在实施用户授权的过程中,拦截器起到的重要的作用,比如:FilterSecurityInterceptor是一个过滤器,针对web资源;MethodSecurityInterceptor是一个AOP拦截器,针对业务方法实施授权;AspectJSecurityInterceptor拦截器,真对AspectJ方法调用实施用户授权!
这些拦截器都会分为三个阶段:事前评估,操控资源,事后审查!
 
事前评估:
下面的配置信息,其中accessDecisionManager属性正是完成事前评估的!
 

<bean id="filterInvocationInterceptor"
        class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
        <property name="authenticationManager" ref="authenticationManager" />
        <property name="accessDecisionManager" ref="httpRequestAccessDecisionManager" />
        <property name="objectDefinitionSource">
            <value>
                PATTERN_TYPE_APACHE_ANT 
                /securedpage.jsp=ROLE_ADMIN
            </value>
        </property>
    </bean>

上述httpRequestAccessDecisionManager的定义如下,开发者需要为AffirmativeBased配置若干投票器(AccessDecisionVoter)

<bean id="httpRequestAccessDecisionManager"
        class="org.acegisecurity.vote.AffirmativeBased">

<property name="allowIfAllAbstainDecisions" value=“false”>
        <property name="decisionVoters">
            <list>
                <ref bean="roleVoter" />
            </list>
        </property>
    </bean>

    <bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter" />
    

其中AffirmativeBased是AccessDecisionManager的子类,还有ConsensusBased,UnanimousBased。他们的含义和使用是有区别的!

AccessDecisionManager采用民主决策机制判断用户是否有权访问目标程序资源,它包含了多个AccessDecisionVoter。在访问决策时每个AccessDecisionVoter都拥有投票权,AccessDecisionManager统计投票结果,并按照某种决策方式根据这些投票结果决定最终是否向用户开放受限资源的访问。Acegi的投票方式有多种,每个Voter对访问权可以投赞成(ACCESS_GRANTED,取值为1),弃权(ACCESS_ABSTAIN,取值为0)或反对(ACCESS_DENIED,取值为-1)!

   Acegi提供了三个具体的类去管理投票结果,他们三个的不同存在于对投票结果的统计上。ConsensusBased类根据非弃权票中赞成票多于反对票来决定是否通过。如果赞成票多于反对票就同意通过!在同意和反对票相同时或都投弃权票时可以根据设置来决定授权与否。配置如下:

<bean id="httpRequestAccessDecisionManager"
        class="org.acegisecurity.vote.ConsensusBased">

<property name="allowIfAllAbstainDecisions" value=“true”>
<property name="allowIfEqualGrantedDeniedDecisions" value=“true”>

        <property name="decisionVoters">
            <list>
                <ref bean="roleVoter" />
            </list>
        </property>
    </bean>

其中属性allowIfAllAbstainDecisions,默认值是false,意味着当配置的投票器只投了弃权票时,不允许继续执行。相比之下,true意味着允许都投弃权票!

allowIfEqualGrantedDeniedDecisions,默认值为true,意味着在赞成票不为0的情况下,且赞成票和反对票相等,则允许通过!false意味着,在赞成票不为0 的情况下,赞成票和反对票相等,不允许通过!

AffirmativeBased类只要有一个或多个投票赞成就会给予通过,同样我们可以设置在都投弃权票的时候是否通过:

<bean id="httpRequestAccessDecisionManager"
        class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value=“false”>
        <property name="decisionVoters">
            <list>
                <ref bean="roleVoter" />
            </list>
        </property>
    </bean>

UnanimousBased类会在所有的非弃权票都投赞成票时才通过,同样的设置出现在都投弃权票的时候:

<bean id="httpRequestAccessDecisionManager"
        class="org.acegisecurity.vote.UnanimousBased">
<property name="allowIfAllAbstainDecisions" value=“true”>
        <property name="decisionVoters">
            <list>
                <ref bean="roleVoter" />
            </list>
        </property>
    </bean>

 Acegi提供了AccessDecisionVoter的两个实现类。对于RoleVoter,我们需要配置它的rolePrefix属性(如ROLE_),当ConfigAttribute以rolePrefix开头时,RoleVoter才投票,否则投弃权票(ACCESS_ABSTAIN)。开始投票时,如果存在一个GrantedAuthority和被访问资源的一个或多个ConfigAttributes完全匹配时,投赞成票,否则投反对票

针对web资源,业务方法,领域对象的用户授权,acegi内置了各种投票器!其中RoleVoter主要针对web资源和业务方法,而AclEntryVoter主要针对领域对象!怎样使用,在后面的章节将!

事后审查:

一旦用户操控到目标资源后,调用结果可能会返回一个领域对象集合,比如,getAll()方法便会返回所有的对象集合!但是并不是任意用户都能够操作到任意领域对象的,我们需要保护他们!

在实施领域对象级的访问控制时(事后审查),acegi的AfterInvocationManager充当了非常主要的角色!

<bean id="contactManagerSecurity" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
      <property name="authenticationManager"><ref bean="authenticationManager"/></property>
      <property name="accessDecisionManager"><ref local="businessAccessDecisionManager"/></property>
      <property name="afterInvocationManager"><ref local="afterInvocationManager"/></property>
      <property name="objectDefinitionSource">
         <value>
            sample.contact.ContactManager.create=ROLE_USER
            sample.contact.ContactManager.getAllRecipients=ROLE_USER
            sample.contact.ContactManager.getAll=ROLE_USER,AFTER_ACL_COLLECTION_READ
            sample.contact.ContactManager.getById=ROLE_USER,AFTER_ACL_READ
            sample.contact.ContactManager.delete=ACL_CONTACT_DELETE
            sample.contact.ContactManager.deletePermission=ACL_CONTACT_ADMIN
            sample.contact.ContactManager.addPermission=ACL_CONTACT_ADMIN
         </value>
      </property>
   </bean>

AfterInvocationManager会对业务对象操作的结果进行审查,比如操作结果中是否含有当前用户无权操作的领域对象,如果有,AfterInvocationManager会从操作结果中将这一领域对象删除!具体怎么回事?后面会讲.

保护web资源:FilterSecurityInterceptor

重新认证:

一旦用户登陆到目标系统后,他便依据其本来的身份进行各种操作,但是在一些场合用户的身份可能会被动态的改变!为了将RDBMS中角色的信息的变化同步到已经登陆的用户中,必须启用alwaysReauthenticate属性,默认值是false:

<bean id="filterInvocationInterceptor" 
           class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
                <property name="alwaysReauthenticate" value="true"></property>

         .......
        </bean>

RoleVoter投票器:

   对于RoleVoter,默认时候只会对那些前缀为ROLE_的角色进行投票处理,吐过角色信息未使用ROLE_开头,我们需要配置它的rolePrefix属性,当ConfigAttribute以rolePrefix开头时,RoleVoter才投票,否则投弃权票(ACCESS_ABSTAIN)。开始投票时,如果存在一个GrantedAuthority和被访问资源的一个或多个ConfigAttributes完全匹配时,投赞成票,否则投反对票

<bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter" >

  

    

开发者配置的各个角色,必须存在相应的投票器对他们进行处理,否则目标Acegi应用不能正常启动!除非validateConfigAttributes属性被显式的置为false。不过建议设置为true!

<bean id="filterInvocationInterceptor" 
           class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
            
         .......
        </bean>

把web资源授权信息存储在RDBMS中:

分析FilterInvocationDefinfitionSource的运行:

<bean id="filterInvocationInterceptor" 
           class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
      <property name="authenticationManager" ref="authenticationManager" />
      <property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
      <property name="objectDefinitionSource">
         <value>
             CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
             PATTERN_TYPE_APACHE_ANT
             /index.jsp=ROLE_ANONYMOUS,ROLE_USER
             /hello.htm=ROLE_ANONYMOUS,ROLE_USER
             /logoff.jsp=ROLE_ANONYMOUS,ROLE_USER
             /acegilogin.jsp*=ROLE_ANONYMOUS,ROLE_USER
                /**=ROLE_USER
         </value>
      </property>
   </bean>

其中objectDefinitionSource属性属于FilterInvocationDefinfitionSource类型!

PATTERN_TYPE_APACHE_ANT:启用了apache ant路径风格的url匹配模式(PathBasedFilterInvocationDefinfitionMap),默认的是正则表达式(RegExpBasedFilterInvocationDefinfitionMap),他们都是FilterInvocationDefinfitionSource的子类!为了通过DBMS维护web资源授权信息,必须自己提供FilterInvocationDefiniftionSource子类!

实现基于DBMS的FilterInvocationDefiniftionSource

建表:

create table webresdb(
 id bigint not null primary key,
 url varchar(60) not null,
 roles varchar(100) not null)

 insert into webresdb values(1,'/securedpage.jsp','ROLE_ADMIN');

编写RdbmsEntryHolder类:

package websourcedb;

import java.io.Serializable;

import org.acegisecurity.ConfigAttributeDefinition;

/**
 * 
 * @author worldheart
 *
 */

public class RdbmsEntryHolder implements Serializable{
    
    //保护的URL模式

    private String url;
    
    //要求的角色集合

    private ConfigAttributeDefinition cad;
    
    public String getUrl() {
        return url;
    }
    
    public ConfigAttributeDefinition getCad() {
        return cad;
    }
    
    public void setUrl(String url) {
        this.url = url;
    }
    
    public void setCad(ConfigAttributeDefinition cad) {
        this.cad = cad;
    }
    
}

从EDBMS装载web资源授权信息,并摆在内存中:

package websourcedb;

import java.sql.ResultSet;
import java.sql.SQLException;

import javax.sql.DataSource;

import org.acegisecurity.ConfigAttributeDefinition;
import org.acegisecurity.SecurityConfig;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.object.MappingSqlQuery;
import org.springframework.util.StringUtils;

/**
 * 
 * @author worldheart
 *
 */

public class RdbmsSecuredUrlDefinition extends MappingSqlQuery{

    protected static final Log logger = LogFactory.getLog(RdbmsSecuredUrlDefinition.class);
    
    protected RdbmsSecuredUrlDefinition(DataSource ds) {
        super(ds, "SELECT url, roles FROM webresdb ORDER BY id");
        logger.debug("进入到RdbmsInvocationDefinition构建器中.........");
        compile();
    }

    protected Object mapRow(ResultSet rs, int rownum)
        throws SQLException {
        logger.debug("抽取webresdb中的记录.........");
        RdbmsEntryHolder rsh = new RdbmsEntryHolder();
        //设置URL

        rsh.setUrl(rs.getString("url").trim());
        
        ConfigAttributeDefinition cad = new ConfigAttributeDefinition();
        
        String[] tokens = 
                StringUtils.commaDelimitedListToStringArray(rs.getString("roles").trim());
        for(int i = 0; i < tokens.length;++i)
            cad.addConfigAttribute(new SecurityConfig(tokens[i]));
        
        //设置角色集合
        rsh.setCad(cad);        
        return rsh;
    }
}

最后开发FilterInvocationDefiniftionSource实现类RdbmsFilterInvocationDefiniftionSource是针对web资源的,因此她实现的supports()方法需要评估FilterInvocation对象,并且采用了Spring EhCache集成服务!

package websourcedb;

import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;

import org.acegisecurity.ConfigAttributeDefinition;
import org.acegisecurity.intercept.web.FilterInvocation;
import org.acegisecurity.intercept.web.FilterInvocationDefinitionSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.core.support.JdbcDaoSupport;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.PathMatcher;

/**
 * 
 * @author worldheart
 * 
 */

public class RdbmsFilterInvocationDefinitionSource extends JdbcDaoSupport
        implements FilterInvocationDefinitionSource {

    protected static final Log logger = LogFactory.getLog(RdbmsFilterInvocationDefinitionSource.class);
    
    private RdbmsSecuredUrlDefinition rdbmsInvocationDefinition;

    private PathMatcher pathMatcher = new AntPathMatcher();
    
    private Ehcache webresdbCache;
    
    public ConfigAttributeDefinition getAttributes(Object object)
            throws IllegalArgumentException {
        if ((object == null) || !this.supports(object.getClass())) {
            throw new IllegalArgumentException("抱歉,目标对象不是FilterInvocation类型");
        }

        // 抽取出待请求的URL

        String url = ((FilterInvocation) object).getRequestUrl();
        
        List list = this.getRdbmsEntryHolderList();
        if (list == null || list.size() == 0)
            return null;

        int firstQuestionMarkIndex = url.indexOf("?");
        if (firstQuestionMarkIndex != -1) {
            url = url.substring(0, firstQuestionMarkIndex);
        }

        Iterator iter = list.iterator();
        while (iter.hasNext()) {
            RdbmsEntryHolder entryHolder = (RdbmsEntryHolder) iter.next();            
            boolean matched = pathMatcher.match(entryHolder.getUrl(), url);

            if (logger.isDebugEnabled()) {
                logger.debug("匹配到如下URL: '" + url + ";模式为 "
                        + entryHolder.getUrl() + ";是否被匹配:" + matched);
            }

            if (matched) {
                return entryHolder.getCad();
            }
        }

        return null;
    }

    public Iterator getConfigAttributeDefinitions() {
        Set set = new HashSet();
        Iterator iter = this.getRdbmsEntryHolderList().iterator();

        while (iter.hasNext()) {
            RdbmsEntryHolder entryHolder = (RdbmsEntryHolder) iter.next();
            set.add(entryHolder.getCad());
        }

        return set.iterator();
    }

    public boolean supports(Class clazz) {
        if (FilterInvocation.class.isAssignableFrom(clazz)) {
            return true;
        } else {
            return false;
        }
    }

    protected void initDao() throws Exception {
        this.rdbmsInvocationDefinition = 
                new RdbmsSecuredUrlDefinition(this.getDataSource());
        if(this.webresdbCache == null)
            throw new IllegalArgumentException("必须为RdbmsFilterInvocationDefinitionSource配置一EhCache缓存");
    }

    private List getRdbmsEntryHolderList(){
        List list = null;
        Element element = this.webresdbCache.get("webres");
        if(element != null){
            list = (List)element.getValue();
        } else {
            list = this.rdbmsInvocationDefinition.execute();
            Element elem = new Element("webres", list);
            this.webresdbCache.put(elem);
        }
        return list;
    }
    
    public void setWebresdbCache(Ehcache webresdbCache) {
        this.webresdbCache = webresdbCache;
    }
    
}

spring中配置一下:

<bean id="filterInvocationInterceptor" 
           class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
        <property name="validateConfigAttributes" value="true" />
        <property name="alwaysReauthenticate" value="true"></property>
      <property name="authenticationManager" ref="authenticationManager" />
      <property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
      <property name="objectDefinitionSource" ref="rdbmsFilterInvocationDefinitionSource" />
   </bean>
   
   <bean id="rdbmsFilterInvocationDefinitionSource" 
           class="websourcedb.RdbmsFilterInvocationDefinitionSource">
           <property name="dataSource" ref="dataSource" />
           <property name="webresdbCache" ref="webresCacheBackend" />
   </bean>

   <bean id="webresCacheBackend" 
           class="org.springframework.cache.ehcache.EhCacheFactoryBean">
      <property name="cacheManager">
         <ref local="cacheManager"/>
      </property>
      <property name="cacheName">
         <value>webresdbCache</value>
      </property>
   </bean>

针对用户授权的标签库:

ifAnyGranted含义为:一旦登陆用户至少具备被指定的角色集合中的一个,则标签包围的html标签便会被渲染:

<authz:authorize ifAnyGranted="ROLE_USER">
    <authz:authentication operation="username" />
    已经具有“ROLE_USER”角色了!<br/>
</authz:authorize>
<authz:authorize ifAnyGranted="ROLE_SUPERVISOR">
    <authz:authentication operation="username" />
    已经具有“ROLE_SUPERVISOR”角色了!<br/>
</authz:authorize>
<authz:authorize ifAnyGranted="ROLE_SUPERVISOR,ROLE_USER">
    <authz:authentication operation="username" />
    已经具有<authz:authentication operation="authorities" />角色了!<br/>
</authz:authorize>

ifAllGranted含义为:只有登陆用户具备被指定的角色集合中的所有角色时,则标签包围的html标签便会被渲染:

<authz:authorize ifAllGranted="ROLE_SUPERVISOR,ROLE_USER">
    <authz:authentication operation="username" />
    已经具有“ROLE_SUPERVISOR,ROLE_USER”角色了!<br/>
</authz:authorize>

ifNotGranted含义为:只有登陆用户不具备被指定的角色集合中的任意角色时,则标签包围的html标签便会被渲染:

<authz:authorize ifNotGranted="ROLE_SUPERVISOR">
    <authz:authentication operation="username" />
    不具有“ROLE_SUPERVISOR”角色!<br/>
</authz:authorize>
<authz:authorize ifNotGranted="ROLE_SUPERVISOR" ifAllGranted="ROLE_USER" 
    ifAnyGranted="ROLE_USER">
    <authz:authentication operation="username" />
    不具有“ROLE_SUPERVISOR”角色,但具有“ROLE_USER”角色!<br/>
</authz:authorize>
<authz:authorize ifNotGranted="ROLE_SUPERVISOR" ifAnyGranted="ROLE_SUPERVISOR">
    不应该出现的内容!<br/>
</authz:authorize>

针对用户认证的acegi标签库:

在acegi-securoty-1.x.jar存档的META-INF目录能够找到authz.tld,在使用之前应该

<%@ taglib prefix="authz" uri="http://acegisecurity.org/authz" %>

(如果开发者使用的jsp1.2规范的web容器,还需要将zuthz.tld抽取到某个地方,并在web.xml中注册):


  http://acegisecurity.org/authz
  
   /WEB-INF/authz.tld
  

下面展示了anthentication标签的使用:

operation(必须给定)和methodPrefix属性,默认时候methodPrefix取get,为了将UserDetails中的getUserName方法的结果展示出来:

把getAuthorities()方法的结果输出到页面上:
0 0
原创粉丝点击