抛砖引玉-使用Acegi实现多种用户登录的一种方案

摘要

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,  true true );

        
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)   编辑   收藏
<script type="text/javascript"> // </script>

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> 
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值