posts - 14, comments - 22, trackbacks - 0, articles - 4
  BlogJava :: 首页 ::  :: 联系 :: 聚合  :: 管理

Spring+Hibernate+Acegi 的初次体验

Posted on 2006-08-24 10:56 誰伴我闖蕩 阅读(3924) 评论(5)  编辑  收藏

到现在我也没有弄明白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(); //这个是用来反映用户角色的关键函数,在子类实现,从而实现多态
}

子类的实现:
======================
package subject.model;

import subject.Constants;

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

//set and get method

 public String getType()
 {
  return Constants.TEACHER;
 }
}
================
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;
 }
}
=================
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提供一种特殊的使用Servlet 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>
                  select user,password,about from user where user = ?        查找用户的查询语句,只需要把你数据库中的用户和密码以及enable相对应上就行
              </value>
          </property>
          <property name="authoritiesByUsernameQuery">
              <value>
                  select user,type from user where user = ?           这里就是把用户和权限对应上,在appfuse中用的两个表,我都放一个表里了,所以就用这一个就行问题的关键是要让它能找到两个字段,构成一个对象
              </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过程中,把它自带的例子弄清楚很关键,希望大家一起学习一起共勉!


评论

# re: Spring+Hibernate+Acegi 的初次体验  回复  更多评论   

2006-08-24 16:29 by 跳跳堂
如果能扩充objectDefinitionSource 介绍就最好了

# re: Spring+Hibernate+Acegi 的初次体验  回复  更多评论   

2006-08-31 16:51 by wangx
<property name="usersByUsernameQuery">
<value>SELECT user_no,user_password FROM SYS_USERS WHERE user_no=?</value>
</property>

请问上面acegi配置文件中的SQL语句的查询条件系统是怎么取得的?

另外,你的例子的登录页面的URL是多少?
URL中是不是要带个/j_acegi_security_check?

# re: Spring+Hibernate+Acegi 的初次体验  回复  更多评论   

2007-01-01 12:45 by itspy
大体上先介绍一下:
httpSessionContextIntegrationFilter:每次request前 HttpSessionContextIntegrationFilter从Session中获取Authentication对象,在request完后, 又把Authentication对象保存到Session中供下次request使用。


有个问题我不太明白,如何从Authentication中取出当前登录的用户名呢,比如要做一个审计,记录当前用户的操作,就一定要得到当前登录用户的名字。

# re: Spring+Hibernate+Acegi 的初次体验  回复  更多评论   

2007-01-12 11:44 by 刺猬[匿名]
To itspy:

Acuthentication中就包含有当前登陆用户的信息,除了用户信息,还包括当前登陆用户已定义的权限信息。

# re: Spring+Hibernate+Acegi 的初次体验  回复  更多评论   

2007-03-16 15:46 by 路过
靠色,楼主怎能这样写?
User user = (User) getSessionObject( request, Constants.USER );
ActionMessages msg = new ActionMessages();
那与其不要Acegi 算了.

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


网站导航:
 
有事在这里给我留言噢!