Spring+Hibernate+Acegi 的初次体验
 
2008-12-09 来源:网络
 

一 :高层设计 核心组件

大多数的企业级应用有四种基本的安全需求。

1 :需要对主体进行鉴别
2 :需要对web请求进行保护
3 :需要对服务层方法进行保护
4 :很多情况下需要对域对象实例进行保护

Acegi系统提供了一个通用的框架,它可以满足所有这四种企业安全级的需求。

到现在我也没有弄明白Acegi里面很多的功能,刚刚开始学的时候我就已经被它那繁琐的配置震慑住了,不过当我动起手来一步步实现的时候,才发现其实它远没有那么难,当然随着学习的深入,会渐渐再发现这一点吧,现在就让我们初学者一切体验Acegi的功能吧!

以我传统的例子为例:

毕业设计选题系统,三种角色:

教师
学生
管理员

我想让他们的登陆都在一个界面下自动识别,而无需进行身份选择,登陆后,他们将分别到各自的

admin.jsp
stu.jsp
teacher.jsp

在数据库中的表结构如下(很多属性略):

id
user
password
type
about

type是用来存储用户的类别,分别有a,t,s分别对应三种角色 , about对应的是acegi里所需要的enable,用户是否可用在model里,我们采用了继承关系:

父类user:

package subject.model;

public abstract class User extends BaseObject
{
     private Integer id;
     private String user;
     private String password;
     private String name;
     private String telphone;

    
// set and get method 
     
     public abstract String getType(); 
// 这个是用来反映用户角色的关键函数,在子类实现,从而实现多态
}

子类的实现(Teacher):

package subject.model;

import subject.Constants;

public class Teacher extends User
{
     private String level;         
// 教师的职称

    
// set and get method

     public String getType() 
{
          
return  Constants.TEACHER;
     }

}

子类的实现(Student):

package subject.model;

import subject.Constants;

public class Student extends User
{
     private static final 
long  serialVersionUID  =  1L;

     private SchoolClass schoolClass;         
// 学生的班级
     private String sn;              // 学生的学号

    
// set and get method
     
     public String getType() 
{
          
return  Constants.STUDENT;
     }

}


子类的实现(Admin):

package subject.model;

import subject.Constants;

public class Admin extends User
{
     private String grade;           
// 管理员的级别
     // set and get method

     public String getType() 
{
          
return  Constants.ADMIN;
     }

}

对于三者所共有的属性在数据库里,都存在一个字段,而依据不同的角色拥有不同的含义,学生的班级则存放在了about里,只要学生有班级,他就able,否则就enable了!而管理员和教师则默认为1!

这种是属于一个继承树存放在一个表的情况,Hibernate的配置如下

< hibernate - mapping >
 
< class name = " subject.model.User "  discriminator - value = " not null " >

  
< id name = " id " >
   
< generator class = " increment "   />
  
</ id >

  
< discriminator column = " type "  type = " character "   />
  
< property name = " user "   />
  
< property name = " password "   />
  
< property name = " name "   />
  
< property name = " telphone "   />

  
< subclass name = " subject.model.Admin "  discriminator - value = " a " >
   
< property name = " grade "  column = " sn "   />
  
</ subclass >

  
< subclass name = " subject.model.Teacher "  discriminator - value = " t " >
   
< property name = " level "  column = " sn "   />
  
</ subclass >

  
< subclass name = " subject.model.Student "  discriminator - value = " s " >
   
< property name = " sn "   />
   
< many - to - one name = " schoolClass "  class = " subject.model.SchoolClass "  
    column
= " about "  update = " false "  insert = " false "   />
  
</ subclass >

 
</ class >
</
hibernate - mapping >

上面的这些都是模型的基础,下面再讲怎么样配合Spring和Acegi实现系统的安全与登陆
在Spring中Hibernate的配置只介绍不说明:

<!-- 定义DBCP数据源 -->

  < bean id = " dataSource "  class = " org.apache.commons.dbcp.BasicDataSource "  destroy - method = " close " >
      
< property name = " driverClassName "  value = " com.mysql.jdbc.Driver "   />
      
< property name = " url "  value = " jdbc:mysql://localhost/subject?useUnicode=true&amp;characterEncoding=gbk "   />
      
< property name = " username "  value = " root "   />
      
< property name = " password "  value = ""   />
      
< property name = " maxActive "  value = " 100 "   />
      
< property name = " maxIdle "  value = " 30 "   />
      
< property name = " maxWait "  value = " 1000 "   />
      
< property name = " defaultAutoCommit "  value = " true "   />
      
< property name = " removeAbandoned "  value = " true "   />
      
< property name = " removeAbandonedTimeout "  value = " 60 "   />
 
</ bean >

 <!-- Hibernate -->

  < bean id = " sessionFactory "  class = " org.springframework.orm.hibernate3.LocalSessionFactoryBean " >
  
< property name = " dataSource "  ref = " dataSource "   />
  
< property name = " mappingResources " >
   
< list >
    
< value > subject / model / User.hbm.xml </ value >
   
</ list >
  
</ property >
  
< property name = " hibernateProperties " >
   
< props >
    
< prop key = " hibernate.dialect " > org.hibernate.dialect.MySQLInnoDBDialect </ prop >
   
</ props >
  
</ property >
 
</ bean >

 
< bean id = " transactionManager "  class = " org.springframework.orm.hibernate3.HibernateTransactionManager " >
  
< property name = " sessionFactory "  ref = " sessionFactory "   />
 
</ bean >

<!--
 Dao对象  -->
<
bean id = " userDao "  class = " subject.dao.hibernate.UserDaoImpl " >
  
< property name = " sessionFactory "  ref = " sessionFactory "   />
 
</ bean >

<!--
 业务逻辑  -->
 
< bean id = " txProxyTemplate "  abstract = " true "  class = " org.springframework.transaction.interceptor.TransactionProxyFactoryBean " >
  
< property name = " transactionManager "  ref = " transactionManager "   />
  
< property name = " transactionAttributes " >
   
< props >
    
< prop key = " save* " > PROPAGATION_REQUIRED </ prop >
    
< prop key = " remove* " > PROPAGATION_REQUIRED </ prop >
    
< prop key = " get* " > PROPAGATION_REQUIRED,readOnly </ prop >
   
</ props >
  
</ property >
 
</ bean >

<
bean id = " userManager "  parent = " txProxyTemplate " >
  
< property name = " target " >
   
< bean class = " subject.service.impl.UserManagerImpl " >
    
< property name = " userDao "  ref = " userDao "   />
   
</ bean >
  
</ property >
 
</ bean >

<!--
 Struts  -->
 
< bean name = " /user "  class = " subject.web.action.UserAction "  singleton = " false " >
  
< property name = " userManager " >
   
< ref bean = " userManager "   />
  
</ property >
 
</ bean >

上面具体的不用了解,无非就是调用和数据库的操作,下面就要对Acegi进行声明了:我不用Ctrl+c和Ctrl+V的方式对Acegi进行介绍,没有意义,随便google就一大堆,我们想主要在这样的系统中需要的安全策略都有哪些?

1 .用户的登陆
2 .防止多个用户登陆一个帐号
3 .用户的注销
4 .防止非法用户的访问

我这个程序所涉及到的只有这些,下面就进行说明:

在web.xml的声明:

     <!--  Acegi安全控制 Filter 配置  -->
    
< 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>

Acegi通过实现了Filter接口的FilterToBeanProxy提供一种特殊的使用Filter的方式,它委托Spring中的Bean -- FilterChainProxy来完成过滤功能,这样就简化了web.xml的配置,并且利用Spring IOC的优势。FilterChainProxy包含了处理认证过程的filter列表,每个filter都有各自的功能。

<!-- ======================== FILTER CHAIN ======================= -->

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

大体上先介绍一下:
httpSessionContextIntegrationFilter:每次request前 HttpSessionContextIntegrationFilter从Session中获取Authentication对象,在request完后, 又把Authentication对象保存到Session中供下次request使用,此filter必须其他Acegi filter前使用,使之能跨越多个请求。

logoutFilter:用户的注销
authenticationProcessingFilter:处理登陆请求
exceptionTranslationFilter:异常转换过滤器
filterInvocationInterceptor:在访问前进行权限检查

这些就犹如在web.xml声明一系列的过滤器,不过当把他们都声明在spring中就可以享受Spring给我们带来的方便了。

下面就是对这些过滤器的具体声明:
只对有用的地方进行声明,别的地方几乎都是默许的
<!-- ======================== FILTER ======================= -->
 

<  bean id  =   "  httpSessionContextIntegrationFilter  "    class   =   "  org.acegisecurity.context.HttpSessionContextIntegrationFilter  "   />  

< bean id  = " logoutFilter "   class   = " org.acegisecurity.ui.logout.LogoutFilter " >  
     
< constructor  -  arg value  = " /index.htm "    />        // 离开后所转向的位置
      < constructor  -  arg  >  
           
< list  >  
               
< bean  class   = " org.acegisecurity.ui.logout.SecurityContextLogoutHandler " />  
             
</ list  >  
     
</ constructor  -  arg  >  
     
< property name  = " filterProcessesUrl "  value  = " /logout.htm "    />     // 定义用户注销的地址,
</ bean  >  

下面的这个过滤器处理登陆请求,我们根据自己的需求有了自己的实现:

< bean id  =   "  authenticationProcessingFilter  "    class   =   "  subject.web.filter.UserAuthenticationProcessingFilter  "   >  
    
< property name  =   "  authenticationManager  "   ref  =   "  authenticationManager  "   />     
    
< property name  =   "  authenticationFailureUrl  "   value  =   "  /login.htm?error=wrong  "   />     // 登陆失败的地址
     < property name  =   "  defaultTargetUrl  "   value  =   "  /login.htm  "   />          // 登陆成功的地址
     < property name  =   "  filterProcessesUrl  "   value  =   "  /j_security_check  "   />         // 登陆请求的地址
     < property name  =   "  userManager  "   ref  =   "  userManager  "   />           // 自己添加的属性,这样就可以访问到我们的业务逻辑
     < property name  =   "  exceptionMappings  "   >     //  出现异常所对应的地址
         < value  >  
            org.acegisecurity.AuthenticationException 
=/  login.htm  ?  error  =  fail     登陆失败               
            org.acegisecurity.concurrent.ConcurrentLoginException 
=/  login.htm  ?  error  =  too        已登陆了
        
</ value  >  
    
</ property  >  
</ bean  >  

作用未知 :

< bean id  =   "  securityContextHolderAwareRequestFilter  "    class   =   "  org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter  "   />  

异常转换过滤器

< bean id  =   "  exceptionTranslationFilter  "    class   =   "  org.acegisecurity.ui.ExceptionTranslationFilter  "   >  
    
< property name  =   "  authenticationEntryPoint  "   >  
        
< bean  class   =   "  org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint  "   >   //进行表单认证;
            <property name = " loginFormUrl "  value = " /login.htm?error=please " /> 
// 如果用户没登陆就想访问,先到这里登陆吧 
            <property name = " forceHttps "  value = " false " /> 
        </bean > 
    </property > 
</bean > 

在访问前进行权限检查

< bean id  =   "  filterInvocationInterceptor  "    class   =   "  org.acegisecurity.intercept.web.FilterSecurityInterceptor  "   >  
    
< property name  =   "  authenticationManager  "   ref  =   "  authenticationManager  "   />         认证服务
    
< property name  =   "  accessDecisionManager  "   >  
        
< bean  class   =   "  org.acegisecurity.vote.AffirmativeBased  "   >  
            
< property name  =   "  allowIfAllAbstainDecisions  "   value  =   "  false  "   />  
            
< property name  =   "  decisionVoters  "   >  
                
< list  >  
                    
< bean  class   =   "  org.acegisecurity.vote.RoleVoter  "   >  
                            
//  这里定义数据库中存放的角色和我们在这里声明的角色间是否需要加个前缀?我没加 
                         < property name  =   "  rolePrefix  "   value  =   ""   />            
                    
</ bean  >  
                
</ list  >  
            
</ property  >  
        
</ bean  >  
    
</ property  >  
    
< property name  =   "  objectDefinitionSource  "   >  
        
< value  >  
                PATTERN_TYPE_APACHE_ANT
                
                 
/  admin.htm  *=  a         这里就是数据库中对应的tyep a
                 
/  student  *=  s           由于没有前缀和数据库里一样
                 
/  teacher  *=  t
        
</ value  >  
    
</ property  >  
</ bean  >  

记录事件

  < bean id = " loggerListener "           class = " org.acegisecurity.event.authentication.LoggerListener " />    

下面就要说明我们的认证服务了,其起到的关键作用就是用来保证用户登陆身份的验证:

它将验证的功能委托给多个Provider,并通过遍历Providers, 以保证获取不同来源的身份认证,若某个Provider能成功确认当前用户的身份,authenticate()方法会返回一个完整的包含用户授权信息的Authentication对象,否则会抛出一个AuthenticationException。

先声明一个管理器吧,在上面的过滤器中都已经用到过了

<!-- ======================== AUTHENTICATION ======================= -->

< bean id  =   "  authenticationManager  "    class   =   "  org.acegisecurity.providers.ProviderManager  "   >  
    
< property name  =   "  providers  "   >  
        
< list  >  
             
//  我仅仅用到 从数据库中读取用户信息验证身份 
             < ref local  =   "  daoAuthenticationProvider  "   />     
        
</ list  >  
    
</ property  >  

    
< property name  =   "  sessionController  "   >  
        
< bean id  =   "  concurrentSessionController  "   
                
class   =   "  org.acegisecurity.concurrent.ConcurrentSessionControllerImpl  "   >  
            
< property name  =   "  maximumSessions  "   >  
                
< value  >   1 </ value  >   //  每个用户同时登陆一位 
             </ property  >  
            
< property name  =   "  sessionRegistry  "   >  
                
< bean id  =   "  sessionRegistry  "    class   =   "  org.acegisecurity.concurrent.SessionRegistryImpl  "   />  
            
</ property  >  
            
< property name  =   "  exceptionIfMaximumExceeded  "   value  =   "  true  "   />  
        
</ bean  >  
    
</ property  >  
</ bean  >  

 来实现唯一的一个Provider,从数据库验证身份

< bean id = "  daoAuthenticationProvider  "   class = "  org.acegisecurity.providers.dao.DaoAuthenticationProvider  " >
    
< property name = "  userDetailsService  " >
        
< bean id = "  jdbcDaoImpl  "
              
class = "  org.acegisecurity.userdetails.jdbc.JdbcDaoImpl  " >
            
< property name = "  dataSource  "  ref = "  dataSource  " />
            
< property name = "  usersByUsernameQuery  " >
                
< value >
                    
//  查找用户的查询语句,只需要把你数据库中的用户和密码以及enable相对应上就行
                    select user,password,about from user where user  =   ?</ value >
            
</ property >
            
< property name = "  authoritiesByUsernameQuery  " >
                
< value >
                    
//  这里就是把用户和权限对应上,在appfuse中用的两个表,
                    
//  我都放一个表里了,
                    
//  所以就用这一个就行问题的关键是要让它能找到两个字段,构成一个对象
                    select user,type from user where user  =   ?
                
</ value >
            
</ property >
        
</ bean >
    
</ property >

    
< property name = "  userCache  " > //  缓存都这么写:
         < bean  class = "  org.acegisecurity.providers.dao.cache.EhCacheBasedUserCache  " >
            
< property name = "  cache  " >
                
< bean  class = "  org.springframework.cache.ehcache.EhCacheFactoryBean  " >
                    
< property name = "  cacheManager  " >
                        
< bean  class = "  org.springframework.cache.ehcache.EhCacheManagerFactoryBean  " />
                    
</ property >
                    
< property name = "  cacheName  "  value = "  userCache  " />
                
</ bean >
            
</ property >
        
</ bean >
    
</ property >
</
bean >   

==============

对于上面登陆请求的处理器我借鉴了springSide,实现的方法如下:

package subject.web.filter;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.ui.webapp.AuthenticationProcessingFilter;
import org.acegisecurity.userdetails.UserDetails;

import subject.Constants;
import subject.model.User;
import subject.service.UserManager;

public class UserAuthenticationProcessingFilter extends
  AuthenticationProcessingFilter
{
 private UserManager userManager;

 public 
void  setUserManager( UserManager userManager )
 
{
  
this .userManager  =  userManager;
 }


 protected 
boolean  requiresAuthentication( HttpServletRequest request ,
   HttpServletResponse response )
 
{
  
boolean  requiresAuth  =  super.requiresAuthentication( request, response );
  HttpSession httpSession 
=   null ;
  
try
  
{
   httpSession 
=  request.getSession(  false  );
  }

  
catch  ( IllegalStateException ignored )
  
{
  }

  
if  ( httpSession  !=   null  )
  
{
   
if  ( httpSession.getAttribute( Constants.USER )  ==   null  )
   
{
    
if  (  ! requiresAuth )
    
{
     SecurityContext sc 
=  SecurityContextHolder.getContext();
     Authentication auth 
=  sc.getAuthentication();
     
if  ( auth  !=   null
       
&&  auth.getPrincipal()  instanceof  UserDetails )
     
{
      UserDetails ud 
=  (UserDetails) auth.getPrincipal(); // 上面声明的sql无非就是要包装成这个对象
      User user  =  userManager.getUser( ud.getUsername() );从业务逻辑里找到用户,放到session里
      httpSession.setAttribute( Constants.USER, user );
     }

    }

   }

  }

  
return  requiresAuth;
 }

}

在看看我的login.htm在登陆成功时是怎么工作的吧?

public class UserAction extends BaseAction
{
 private UserManager mgr;

 public 
void  setUserManager( UserManager mgr )
 
{
  
this .mgr  =  mgr;
 }


 public ActionForward login( ActionMapping mapping , ActionForm form ,
   HttpServletRequest request , HttpServletResponse response )
   throws Exception
 
{
  User user 
=  (User) getSessionObject( request, Constants.USER );
  ActionMessages msg 
=   new  ActionMessages();
  
if  ( user  !=   null  )
  
{
   
return   new  ActionForward(  user.getType()  +   " .htm " true  );成功就去type.htm
  }

  
else
  
{
   String error 
=  getParameter( request, Constants.ERROR );
   
if  ( error  !=   null  )对于不同的错误,都加以提示
   
{
    
if  ( error.equalsIgnoreCase(  " wrong "  ) )
     msg.add( 
" msg " new  ActionMessage(  " fail.login.wrong "  ) );
    
else   if  ( error.equalsIgnoreCase(  " too "  ) )
     msg.add( 
" msg " new  ActionMessage(  " fail.login.too "  ) );
    
else   if  ( error.equalsIgnoreCase(  " fail "  ) )
     msg.add( 
" msg " new  ActionMessage(  " fail.login.fail "  ) );
    
else
     msg.add( 
" msg " new  ActionMessage(  " fail.login.please "  ) );
   }

   
else
    msg.add( 
" msg " new  ActionMessage(  " fail.login.please "  ) );
  }

  saveErrors( request, msg );
  
return  mapping.findForward(  " fail "  );
 }


}

当然,Acegi需要介绍的东西太多了,我只把我这次认为有必要解释的东西写在了上面让大家来参考,作为能google到的东西,比如对于认证的方式还有很多,我就没有详细的介绍,在学习Acegi过程中,把它自带的例子弄清楚很关键,希望大家一起学习一起共勉!


火龙果软件/UML软件工程组织致力于提高您的软件工程实践能力,我们不断地吸取业界的宝贵经验,向您提供经过数百家企业验证的有效的工程技术实践经验,同时关注最新的理论进展,帮助您“领跑您所在行业的软件世界”。
资源网站: UML软件工程组织