注:本文转载,尊重原创,原创地址:[url]http://www.blogjava.net/redhatlinux/archive/2008/09/01/226010.html[/url]
 
前一篇文章里介绍了Spring Security的一些基础知识,相信你对Spring Security的工作流程已经有了一定的了解,如果你同时在读源代码,那你应该可以认识的更深刻。在这篇文章里,我们将对Spring Security进行一些自定义的扩展,比如自定义实现UserDetailsService,保护业务方法以及如何对用户权限等信息进行动态的配置管理。
说明:
如果你通过Google搜索,可以找到很 多类似主题的文章,本文的目的在于通过这些实例来介绍的工作
原理,我觉得这些才是最重要的。相信你在读完本文之后应该可以按照自己的想法去扩展Spring Secu
rity,这也是我写这两篇文章的目的。希望能对 初学者们有所帮助。
 
废话少说,咱们直接进入正题。

一 自定义UserDetailsService实现

UserDetailsService 接口,这个接口中只定义了唯一的UserDetails loadUserByUsername(String username)方法,它通过用户名来获取整个UserDetails对象。
前一篇文章已经介绍了系统提供的默认实现方式InMemoryDaoImpl ,它从配置文件中读取用户的身份信息(用户名,密码等),如果你的客户想修改用户信息,就需要直接修改配置文件(你需要告诉用户配置文件的路径,应该在什么地方修改,如何把明文密码通过MD5 加密以及如何重启服务器等)。听起来是不是很费劲啊!
在实际应用中,我们可能需要提供动态的方式来获取用户身份信息,最常用的莫过于数据库了,当然也可以是LDAP 服务器等。本文首先介绍系统提供的一个默认实现类 JdbcDaoImpl org.springframework.security.userdetails.jdbc. JdbcDaoImpl ),它通过用户名从数据库中获取用户身份信息,修改配置文件,将 userDetailsService Bean 的配置修改如下:
1  < bean  id ="userDetailsService"
2  class ="org.springframework.security.userdetails.jdbc.JdbcDaoImpl"
3  p:dataSource-ref ="dataSource"
4  p:usersByUsernameQuery ="select userName, passWord, enabled, from users where userName=?"
5  p:authoritiesByUsernameQuery ="select 
6  u.userName,r.roleName from users u,roles 
7  r,users_roles ur where u.userId=ur.userId and 
8  r.roleId=ur.roleId and u.userName=?" />
JdbcDaoImpl 类继承自Spring Framework的JdbcDaoSupport类并实现了UserDetailsService接口,因为从数据库中读取信息,所以首先需要一个数据源对象,这里不在多说,这里需要重点介绍的是usersByUsernameQueryauthoritiesByUsernameQuery,属性,它们的值都是一条SQL语句,JdbcDaoImpl类通过SQL从数据库中检索相应的信息,usersByUsernameQuery属性定义了通过用户名检索用户信息的SQL语句,包括用户名,密码以及用户是否可用,authoritiesByUsernameQuery属性定义了通过用户名检索用户权限信息的SQL语句,这两个属性都引用一个MappingSqlQuery (请参考Spring Framework 相关资料) 例,MappingSqlQuery mapRow() 方法将一个ResultSet (结果集)中的字段映射 一个 , Spring Security 为我们提供了默认的数据库表,如下图所示 (摘自《Spring in Action》)
<!--[if supportFields]--> 1<!--[if supportFields]-->   JdbcDaoImp 数据库表
如果我们需要获取用户的其它信息就需要自己来扩展系统的默认实现,首先应该了解一下UserDetailsService实现的原理,还是要回到源代码,以下是JdbcDaoImpl类的部分代码:
 1  private   class  UsersByUsernameMapping  extends  MappingSqlQuery {
 2 
 3        protected  UsersByUsernameMapping(DataSource ds) {
 4 
 5               super (ds, usersByUsernameQuery);
 6 
 7              declareParameter( new  SqlParameter(Types.VARCHAR));
 8 
 9              compile();
10 
11       }
12 
13        protected  Object mapRow(ResultSet rs,  int  rownum)  throws  SQLException {
14 
15              String username  =  rs.getString( 1 );
16 
17              String password  =  rs.getString( 2 );
18 
19               boolean  enabled  =  rs.getBoolean( 3 );
20 
21              UserDetails user  =   new  User(username, password, enabled,  true ,
22 
23                 true true new  GrantedAuthority[] { new  GrantedAuthorityImpl( " HOLDER " )});
24 
25               return  user;
26       }
27 
28  }
也许你已经看出什么来了,对了,系统返回的UserDetails对象就是从这里来的,这就是读源代码的好处,DaoAuthenticationProvider提供者通过调用自己的authenticate(Authentication authentication)方法将用户在登录页面输入的用户信息与这里从数据库获取的用户信息进行匹配,如果匹配成功则将用户的权限信息赋给Authentication对象并将其存放在SecurityContext中,供其它请求使用。
那么我们要扩展获得更多的用户信息,就要从这里下手了(数据库表这里不在列出来,可以参考项目的WebRoot/db目录下的schema.sql文件)。比如我们自己的数据库设计中是通过一个loginId和用户名来登录或者我们需要额外ID,EMAIL地址等信息, MySecurityJdbcDaoImpl 实现如下:

 1  protectedclass UsersByUsernameMapping  extends  MappingSqlQuery {
 2 
 3          protected  UsersByUsernameMapping(DataSource ds) {
 4 
 5              super (ds, usersByUsernameQuery);
 6 
 7             declareParameter( new  SqlParameter(Types.VARCHAR));
 8 
 9             compile();
10 
11         }
12 
13          protected  Object mapRow(ResultSet rs,  int  rownum)  throws  SQLException {
14 
15              //  TODO Auto-generated method stub
16 
17             String userName  =  rs.getString( 1 );
18 
19             String passWord  =  rs.getString( 2 );
20 
21              boolean  enabled  =  rs.getBoolean( 3 );
22 
23             Integer userId  =  rs.getInt( 4 );
24 
25             String email  =  rs.getString( 5 );
26 
27         MyUserDetails user  =   new  MyUser(userName, passWord, enabled,  true
28                                                 true , true new  GrantedAuthority[]{ new  
29                                                 GrantedAuthorityImpl( " HOLDER " )});
30 
31             user.setEmail(email);
32 
33             user.setUserId(userId);
34 
35              return  user;
36 
37         }
38 
39  }
如果你已经看过源代码,你会发现这里只是其中的一部分代码 ,具体的实现请看项目的 MySecurityJdbcDaoImpl 类实现,以及 MyUserDetails MyUser 类,这里步在一一列出。
如果使用Hibernate来操作数据库,你也可以从你的DAO中获取用户信息,最后你只要将存放了用户身份信息和权限信息的列表(List)返回给系统就可以。
提示:
这里没有介绍更多的细节问题,主要还是想你自己能通过读源代码来加深理解,本人水平也有限,
相信你从源代码中能悟出更多的有价值的东西。
 
    每当用户请求一个受保护的资源时,就会调用认证管理器以获取用户认证信息,但是如果我们的用户信息保存在数据库中,那么每次请求都从数据库中获取信息将会影响系统性能,那么将用户信息进行缓存就有必要了,下面就介绍如何在Spring Security中使用缓存。

缓存用户信息

查看 AuthenticationProvider 接口的实现类 AbstractUserDetailsAuthenticationProvider 抽象类(我们配置文件中配置的 DaoAuthenticationProvider 类继承了该类)的源代码,会有一行代码:
1  UserDetails user  =   this .userCache.getUserFromCache(username);
DaoAuthenticationProvider 认证提供者使用 UserCache接口 的实现来实现对用户信息的缓存,修改DaoAuthenticationProvider 的配置如下:
1  < bean  id ="daoAuthenticationProvider"
2  class ="org.springframework.security.providers.dao.DaoAuthenticationProvider"
3      p:userCache-ref ="userCache"
4      p:passwordEncoder-ref ="passwordEncoder"
5      p:userDetailsService- ref ="userDetailsService" />
这里我们加入了对 userCache Bean 的引用,userCache 使用 Ehcache 来实现对用户信息的缓存。userCache 配置如下:
 1  < bean  id ="userCache"
 2  class ="org.springframework.security.providers.dao.cache.EhCacheBasedUserCache"
 3      p:cache-ref ="cache" />
 4  < bean  id ="cache"
 5  class ="org.springframework.cache.ehcache.EhCacheFactoryBean"
 6      p:cacheManager-ref ="cacheManager"
 7      p:cacheName ="userCache" />
 8  < bean  id ="cacheManager"
 9  class ="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
10      p:configLocation ="classpath:ehcache.xml" >
11  </ bean >
    我们这里使用的是 EhCacheBasedUserCache ,也就是用 EhCache 实现缓存的,另外系统还提供了一个默认的实现类 NullUserCache 类,我们可以通过源代码了解到,无论上面使用这个类都返回一个 null 值,也就是不使用缓存。

保护业务方法

从第一篇文章中我们已经了解到,Spring Security使用Servlet过滤器来拦截用户的请求来保护WEB资源,而这里却是使用Spring 框架的AOP来提供对方法的声明式保护。它通过一个拦截器来拦截方法调用,并调用方法安全拦截器来保护方法。
在介绍之前,我们先回忆一下过滤器安全拦截器是如何工作的。过滤器安全拦截器首先调用AuthenticationManager认证管理器认证用户信息,如果用过认证则调用AccessDecisionManager访问决策管理器来验证用户是否有权限访问objectDefinitionSource中配置的受保护资源。
首先看看如何配置方法安全拦截器,它和过滤器安全拦截器一方继承自AbstractSecurityInterceptor抽象类(请看源代码),如下:
1  < bean  id ="methodSecurityInterceptor"
2  class ="org.springframework.security.intercept.method.aopalliance.MethodSecurityInterceptor"
3      p:authenticationManager-ref ="authenticationManager"
4      p:accessDecisionManager-ref ="accessDecisionManager" >
5       < property  name ="objectDefinitionSource" >
6          < value >    
              com.test.service.UserService.get*=ROLE_SUPERVISOR
7          </ value >
8       </ property >
9  </ bean >
这段代码是不是很眼熟啊,哈哈~,这和我们配置的过滤器安全拦截器几乎完全一样,方法安全拦截器的处理过程实际和过滤器安全拦截器的实现机制是相同的,这里就在累述,详细介绍请参考< Spring Security 学习总结一>中相关部分。但是也有不同的地方,那就是这里的objectDefinitionSource 的配置,在等号前面的不在是URL 资源,而是需要保护的业务方法,等号后面还是访问该方法需要的用户权限。我们这里配置的 com.test.service.UserService.get* 表示对 com.test.service 包下 UserService 类的所有以 get 开头的方法都需要 ROLE_SUPERVISOR 权限才能调用。这里使用了 提供的实现方法 MethodSecurityInterceptor ,系统还给我们提供了 aspectj 的实现方式,这里不在介绍(我也正在学 ),读者可以参考其它相关资料。
之前已经提到过了,Spring Security使用Spring 框架的AOP来提供对方法的声明式保护,即拦截方法调用,那么接下来就是创建一个拦截器,配置如下:
 1  < bean  id ="autoProxyCreator"
 2  class ="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" >
 3       < property  name ="interceptorNames" >
 4          < list >
 5          < value > methodSecurityInterceptor </ value >
 6          </ list >
 7       </ property >
 8       < property  name ="beanNames" >
 9          < list >
10              < value > userService </ value >
11          </ list >
12       </ property >
13  </ bean >
userService 是我们在applicationContext.xml中配置的一个Bean,AOP的知识不是本文介绍的内容。到这里保护业务方法的配置就介绍完了