空间站

北极心空

  BlogJava :: 首页 :: 联系 :: 聚合  :: 管理
  15 Posts :: 393 Stories :: 160 Comments :: 0 Trackbacks
摘要

Acegi提供了多种身份验证方式(表单验证,CAS等),但只允许一种用户登录,而就个人了解,有一些系统是需要多种用户登录的。比如企业的员工需要登录并使用系统,企业也允许客户登录系统并使用有限的功能。以下尝试剖析Acegi的表单验证过程,并给出一种允许多种用户登录的方案。本方案基本达到“能用”的目的,但不一定是最佳方案。希望这篇文章能起到抛砖引玉的作用,给各位朋友一点参考,也希望各位提出有益的建议。

Acegi的表单验证方式简要分析

一个使用Acegi的表单验证的登录页面通常需要在表单提交时request的j_username和j_password参数赋值,即用户名和密码,而表单则提交到Acegi设定到验证地址。例如:

<form method="post" id="loginForm" action="<c:url value='/j_security_check'/>" >
        
<input type="text" name="j_username" id="j_username" />

        
<input type="password" name="j_password" id="j_password" />

        
<input type="submit" name="login" value="Login" />
</form>

服务器的Servlet容器收到请求后会传递给Acegi的FilterToBeanProxy,这需要在web.xml中进行配置。例如:

<filter>
    
<filter-name>securityFilter</filter-name>
    
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
    
<init-param>
        
<param-name>targetClass</param-name>
        
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
    
</init-param>
</filter>
<filter-mapping>
    
<filter-name>securityFilter</filter-name>
    
<url-pattern>/*</url-pattern>
</filter-mapping>

FilterToBeanProxy基本上只起到调用转发的作用。在它的doFilter方法中会找到类型为FilterChainProxy的bean,调用后者的doFilter方法,同时把request、response会chain参数都传递过去。代码如下:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    
throws IOException, ServletException {
    
if (!initialized) {
        doInit();
    }

    delegate.doFilter(request, response, chain);
}

上面的代码中的delegate就是找到的类型FilterChainProxy的bean。FilterChainProxy的典型配置如下:

<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
    
<property name="filterInvocationDefinitionSource">
        
<value>
            CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
            PATTERN_TYPE_APACHE_ANT
            /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,
        
</value>
    
</property>
</bean>

对于上面的配置,引用一段Acegi联机帮助中的说明来帮助理解:

Internally Acegi Security will use a PropertyEditor to convert the string presented in the above XML fragment into a FilterInvocationDefinitionSource object. What's important to note at this stage is that a series of filters will be run - in the order specified by the declaration - and each of those filters are actually the <bean id> of another bean inside the application context.

实际上,FilterChainProxy的doFilter方法会执行如下处理:
1.读取配置,如果配置为空,则直接调用chain.doFilter,返回
2.如果配置不为空,则根据配置找到各个bean,放入Filter数组中。如果配置中没有配置任何bean,则直接调用chain.doFilter,返回
3.FilterChainProxy创建一个VirtualFilterChain对象,并将chain封装为一个FilterInvocation对象,将它和Filter数组一起传递给VirtualFilterChain的构造函数。VirtualFilterChain的构造函数初始化了一个指针currentPosition,指向Filter数组的第一个元素additionalFilters[0]
4.FilterChainProxy调用VirtualFilterChain的doFilter方法,在该方法中将指针currentPosition前移,调用additionalFilters[0]的doFilter方法。注意这里VirtualFilterChain把自身作为参数传递给additionalFilters[0]的doFilter方法,这样additionalFilters[0]的doFilter方法最后会调用VirtualFilterChain的doFilter方法,这样控制就又回到了VirtualFilterChain!于是VirtualFilterChain又将currentPosition前移,调用additionalFilters[1]的doFilter方法......
5.当additionalFilters中所有元素的doFilter都执行完毕,VirtualFilterChain执行fi.getChain().doFilter,而fi.getChain()的值就是FilterChainProxy的doFilter方法中的参数chain的值。这样我们就理解了FilterChainProxy是怎样让调用兜了个圈,又传递出去的。

重新回到FilterChainProxy的配置,看到它调用了authenticationProcessingFilter这个Filter。让我们看看它的配置:

<bean id="authenticationProcessingFilter"
    class
="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
    
<property name="authenticationManager" ref="authenticationManager"/>
    
<property name="authenticationFailureUrl" value="/login.jsp?error=true"/>
    
<property name="defaultTargetUrl" value="/"/>
    
<property name="filterProcessesUrl" value="/j_security_check"/>
    
<property name="rememberMeServices" ref="rememberMeServices"/>
</bean>

authenticationProcessingFilter的其中一个作用就是获取客户端提交的用户名和密码,将它们封装为一个Token,传递给authenticationManager的authenticate方法,由后者负责验证。

看看authenticationManager的配置:

<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
    
<property name="providers">
        
<list>
            
<ref local="daoAuthenticationProvider"/>
            
<ref local="anonymousAuthenticationProvider"/>
            
<ref local="rememberMeAuthenticationProvider"/>
        
</list>
    
</property>
</bean>

authenticationManager依次调用每个provider的authenticate方法。如果某个provider验证成功则返回;如果所有的验证都不成功,则抛出异常。

让我们看看daoAuthenticationProvider的配置:

<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
     
<property name="userDetailsService" ref="userDao"/>
     
<property name="passwordEncoder" ref="passwordEncoder"/>
</bean>

daoAuthenticationProvider在authenticate方法中调用retrieveUser方法取得用户信息,执行基本的验证,然后调用additionalAuthenticationChecks执行附加的验证(比如验证密码是否正确)。在retrieveUser方法中调用userDetailsService的loadUserByUsername方法取得用户信息,而userDetailsService是一个名为userDao的bean。让我们看看userDao的配置:

<bean id="userDao" class="cn.net.cogent.summer.extension.appfuse.dao.hibernate.EmployeeDaoHibernate">
    
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

userDao实现了Acegi的UserDetailsService接口,该接口只有loadUserByUsername方法。loadUserByUsername方法根据传入的username取得相应的Employee对象(Employee实现了UserDetails接口),该对象返回给daoAuthenticationProvider,由它和authenticationManager联合完成验证的任务。

以上对Acegi对表单验证过程进行了简单对分析,限于篇幅,无法深入分析源码。但从配置可以画出验证过程的对象图如下:



从图中可以看出,尽管Acegi调用了多个Filter来完成验证过程,关键点却在三处:
1.在客户端输入身份验证信息,包括用户名和密码
2.AuthenticationProcessingFilter取出用户名和密码,封装为一个Token往后传递
3.DaoAuthenticationProvider从系统中找出用户资料,并和ProviderManager一起执行验证

实现多种用户登录

很明显,要让系统识别不同种类的用户,必须设立一个用户类型标志。问题就转化为:
1.用户在客户端输入身份信息时系统就必须设立相应的标志
2.该标志如何传递到DaoAuthenticationProvider
3.DaoAuthenticationProvider如何识别该标志,并从相应类型的用户中找到指定用户

我不打算改动Acegi的源码,只打算扩展出我需要的功能。

首先在登录页面中加入用户类型标志j_userkind。在登录页面中加入如下代码:

<input type="hidden" name="j_userkind" id="j_userkind" value="0">

其中0代码员工,1代码客户。可以考虑在登录页面中增加一个选项,如果用户要以员工身份登录,则把j_userkind置为0;如果用户要以客户身份登录,则把j_userkind置为1。也可以提供两个登录页面,其中一个员工专用(j_userkind被强制置为0),另一个客户专用(j_userkind被强制置为1)

系统如何根据收到的用户类型标志去读取指定的用户呢?如果在代码中写死(比如当用户类型标志=0时,读取员工;当用户类型标志=1时,读取客户)非常不好,还是通过配置来确定比较灵活。首先编写UserKindComparisonAware接口:

package cn.net.cogent.summer.extension.acegisecurity.providers;

public
 interface UserKindComparisonAware {

    
public void setExpectedUserKind(String expectedUserKind);
      
public void setCurrentUserKind(String currentUserKind);

}

该接口说明实现类需要实现两个方法,setExpectedUserKind用于接受一个期望的用户类型标志(通常该标志通过配置来设置),setCurrentUserKind用于接受当前登录用户的用户类型标志(系统在运行时捕获,并传递给实现类)

编写MKUDaoAuthenticationProvider类:

package cn.net.cogent.summer.extension.acegisecurity.providers.dao;

import cn.net.cogent.summer.extension.acegisecurity.BadUserKindException;
import cn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware;

import org.acegisecurity.AuthenticationException;
import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import org.acegisecurity.providers.dao.DaoAuthenticationProvider;
import org.acegisecurity.userdetails.UserDetails;

import cn.net.cogent.summer.util.LoggerUtil;

public class MKUDaoAuthenticationProvider extends DaoAuthenticationProvider implements
    UserKindComparisonAware {

    
private String expectedUserKind;
      
private String currentUserKind;

    
public String getExpectedUserKind() {
          
return expectedUserKind;
    }
    
public void setExpectedUserKind(String expectedUserKind) {
          
this.expectedUserKind = expectedUserKind;
    }

      
public String getCurrentUserKind() {
            
return currentUserKind;
      }
      
public void setCurrentUserKind(String currentUserKind) {
            
this.currentUserKind = currentUserKind;
      }

    
protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication) 
throws AuthenticationException {
        LoggerUtil.getLogger().debug(
"expectedUserKind = '" + expectedUserKind + "', currentUserKind = '" + currentUserKind + "'");
        
if (currentUserKind.equals(expectedUserKind))
            
super.additionalAuthenticationChecks(userDetails, authentication);
        
else
            
throw new BadUserKindException(
                
"Flag UserKind does not match");
    }
}

该类继承自DaoAuthenticationProvider并实现UserKindComparisonAware接口,在additionalAuthenticationChecks方法中判断当前登录用户的用户类型标志与期望的用户类型标志是否一致,如果一致则执行父类的additionalAuthenticationChecks,完成验证;否则抛出一个BadUserKindException异常,表明验证失败。BadUserKindException继承自org.acegisecurity.AuthenticationException,具体的代码略

在applicationContext.xml中删除daoAuthenticationProvider相关的配置,增加如下配置:

<bean id="customerDaoAuthenticationProvider" class="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider">
     
<property name="userDetailsService" ref="customerDao"/>
     
<property name="passwordEncoder" ref="passwordEncoder"/>
     
<property name="expectedUserKind" value="1"/>
</bean>

<bean id="userDaoAuthenticationProvider" class="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider">
     
<property name="userDetailsService" ref="userDao"/>
     
<property name="passwordEncoder" ref="passwordEncoder"/>
     
<property name="expectedUserKind" value="0"/>
</bean>

可以看出customerDaoAuthenticationProvider仅用于验证客户(其expectedUserKind被指定为1),而userDaoAuthenticationProvider仅用于验证员工(其expectedUserKind被指定为0)。customerDao的配置如下:

<bean id="customerDao" class="cn.net.cogent.summer.extension.appfuse.dao.hibernate.CustomerDaoHibernate">
    
<property name="sessionFactory" ref="sessionFactory"/>
</bean>

CustomerDaoHibernate的代码如下:

package cn.net.cogent.summer.extension.appfuse.dao.hibernate;

import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.UserDetailsService;
import org.acegisecurity.userdetails.UsernameNotFoundException;

import cn.net.cogent.summer.model.Customer;
import org.appfuse.dao.hibernate.GenericDaoHibernate;
import org.springframework.dao.DataAccessException;

import java.util.List;

public class CustomerDaoHibernate extends GenericDaoHibernate<Customer, Long> implements UserDetailsService {

    
public CustomerDaoHibernate() {
        
super(Customer.class);
    }

    
public UserDetails loadUserByUsername(String username)
        
throws UsernameNotFoundException, DataAccessException {
        List
<Customer> users = getHibernateTemplate().find("from Customer where username=?", username);
        
if (users == null || users.isEmpty()) {
            
throw new UsernameNotFoundException("Customer '" + username + "' not found");
        } 
else {
            
return (UserDetails) users.get(0);
        }
    }
}


可以看出CustomerDaoHibernate是取得一个Customer对象(实现了UserDetails接口),而不是Employee。

修改authenticationManager的配置如下:

<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
    
<property name="providers">
        
<list>
            
<ref local="customerDaoAuthenticationProvider"/>
            
<ref local="userDaoAuthenticationProvider"/>
            
<ref local="anonymousAuthenticationProvider"/>
            
<ref local="rememberMeAuthenticationProvider"/>
        
</list>
    
</property>
</bean>

在哪里捕获当前登录用户的用户类型标志,并传递给MKUDaoAuthenticationProvider呢?我决定增加一个名为PreAuthenticationProcessingFilter的Filter,放在AuthenticationProcessingFilter之前,代码如下:

package cn.net.cogent.summer.extension.acegisecurity.ui.webapp;

import cn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactoryUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

import java.io.IOException;

import java.util.Iterator;
import java.util.Map;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;

public class PreAuthenticationProcessingFilter implements Filter, ApplicationContextAware {

    
public static final String ACEGI_SECURITY_FORM_USERKIND = "j_userkind";

    
private FilterConfig filterConfig;
    
private boolean initialized = false;
    
private Map targetBeans;
    
private String targetClass;
    
private ApplicationContext applicationContext;

    
public String getTargetClass() {
        
return targetClass;
    }
    
public void setTargetClass(String targetClass) {
        
this.targetClass = targetClass;
    }

    
public void setApplicationContext(ApplicationContext applicationContext) {
        
this.applicationContext = applicationContext;
    }

    
public void destroy() {
    }

    
public void init(FilterConfig filterConfig) throws ServletException {
        
this.filterConfig = filterConfig;
    }

    
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException,
            ServletException {
        
if (!(request instanceof HttpServletRequest)) {
            
throw new ServletException("Can only process HttpServletRequest");
        }

        
if (!initialized) {
            doInit();
        }

        String userKind 
= obtainUserKind((HttpServletRequest)request);
        
for (Iterator it = targetBeans.values().iterator(); it.hasNext();) {
             UserKindComparisonAware comparison 
= (UserKindComparisonAware)it.next();
             comparison.setCurrentUserKind(userKind);
        }

        chain.doFilter(request, response);
    }

    
private synchronized void doInit() throws ServletException {
        
if ((targetClass == null|| "".equals(targetClass)) {
            
throw new ServletException("targetClass must be specified");
        }

        Class _targetClass;

        
try {
            _targetClass 
= Thread.currentThread().getContextClassLoader().loadClass(targetClass);
        } 
catch (ClassNotFoundException ex) {
            
throw new ServletException("Class of type " + targetClass + " not found in classloader");
        }

        targetBeans 
= BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, _targetClass, truetrue);

        
if (targetBeans.size() == 0) {
            
throw new ServletException("Bean context must contain at least one bean of type " + targetClass);
        }

        
for (Iterator it = targetBeans.entrySet().iterator(); it.hasNext();) {
              Map.Entry entry 
= (Map.Entry)it.next();
                
if (!(entry.getValue() instanceof UserKindComparisonAware)) {
                    
throw new ServletException("Bean '" + entry.getKey() +
                        
"' does not implement cn.net.cogent.summer.extension.acegisecurity.providers.UserKindComparisonAware");
                }
        }

        
// Set initialized to true at the end of the synchronized method, so
        
// that invocations of doFilter() before this method has completed will not
        
// cause NullPointerException
        initialized = true;
    }

    
protected String obtainUserKind(HttpServletRequest request) {
        
return request.getParameter(ACEGI_SECURITY_FORM_USERKIND);
    }
}

PreAuthenticationProcessingFilter需要在初始化参数中指定targetClass,该参数的值是一个类,该类实现了UserKindComparisonAware接口。PreAuthenticationProcessingFilter找到容器中所有该类的实例,并把捕获的当前登录用户的用户类型标志赋值给它们。PreAuthenticationProcessingFilter的配置如下:

<bean id="preAuthenticationProcessingFilter"
    class
="cn.net.cogent.summer.extension.acegisecurity.ui.webapp.PreAuthenticationProcessingFilter">
    
<property name="targetClass"
        value
="cn.net.cogent.summer.extension.acegisecurity.providers.dao.MKUDaoAuthenticationProvider"/>
</bean>

还需要把preAuthenticationProcessingFilter加入到filterChainProxy的配置中:

<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
    
<property name="filterInvocationDefinitionSource">
        
<value>
            CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
            PATTERN_TYPE_APACHE_ANT
            /**=,preAuthenticationProcessingFilter,authenticationProcessingFilter,
        
</value>
    
</property>
</bean>


注意把它放在authenticationProcessingFilter的前面

至此我们初步实现了使用Acegi实现多种用户登录
posted on 2007-09-18 22:19 雨奏 阅读(936) 评论(6)  编辑  收藏

FeedBack:
# re: 抛砖引玉-使用Acegi实现多种用户登录的一种方案 2007-09-19 12:02 千里冰封
就为了一个登录,这样配置有点复杂了吧:)  回复  更多评论
  
# re: 抛砖引玉-使用Acegi实现多种用户登录的一种方案 2007-09-19 13:25 雨奏
@千里冰封
请问怎样配置会更好呢?能简要说说你的办法吗?  回复  更多评论
  
# re: 抛砖引玉-使用Acegi实现多种用户登录的一种方案 2007-09-19 16:39 西滨
实现多种用户登录倒不难,难的是有了多种用户(像本文的员工和客户)之后,怎么处理不同用户的角色、权限?  回复  更多评论
  
# re: 抛砖引玉-使用Acegi实现多种用户登录的一种方案 2007-09-19 21:39 雨奏
@西滨
我倒是觉得处理角色和权限不难。原本系统中员工的角色、权限是如何授予的,客户的角色、权限可以用类似的方法处理  回复  更多评论
  
# re: 抛砖引玉-使用Acegi实现多种用户登录的一种方案 2007-09-20 11:30 Java初心
acegi的dao验证本来就支持USERROLE的吧

<bean id="jdbcDaoImpl"
class="org.acegisecurity.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource">
<ref bean="dataSource" />
</property>
<property name="usersByUsernameQuery">
<value>
SELECT USERID, PASSWORD,1 FROM T_USER_ROLE
WHERE USERID=?
</value>
</property>
<property name="authoritiesByUsernameQuery">
<value>
SELECT USERID,USERROLE FROM T_USER_ROLE WHERE
USERID=?
</value>
</property>
</bean> 
posted on 2008-06-19 10:36 芦苇 阅读(507) 评论(0)  编辑  收藏 所属分类: SpringJAVA

只有注册用户登录后才能发表评论。


网站导航: