前一篇文章里介绍了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 的配置修改如下:
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接口,因为从数据库中读取信息,所以首先需要一个数据源对象,这里不在多说,这里需要重点介绍的是usersByUsernameQuery 和authoritiesByUsernameQuery ,属性,它们的值都是一条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类的部分代码:
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 实现如下:
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 类继承了该类)的源代码,会有一行代码:
DaoAuthenticationProvider认证提供者使用 UserCache接口 的实现来实现对用户信息的缓存,修改 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配置如下:
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 抽象类(请看源代码),如下:
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来提供对方法的声明式保护,即拦截方法调用,那么接下来就是创建一个拦截器,配置如下:
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的知识不是本文介绍的内容。到这里保护业务方法的配置就介绍完了。
四 将资源放在数据库中
现在,你的用户提出了新的需求,它们需要自己可以给系统用户分配或者取消权限。其实这个并不是什么新鲜事,作为开发者,你也应该为用户提供这样的功能。那么我们就需要这些受保护的资源和用户权限等信息都是动态的,你可以选择把它们存放在数据库中或者LDAP服务器上,本文以数据库为例,介绍如何实现用户权限的动态控制。
通过前面的介绍,你可能也注意到了,不管是 MethodSecurityInterceptor 还是FilterSecurityInterceptor 都使用authenticationManager 和accessDecisionManager 属性用于验证用户,并且都是通过使用objectDefinitionSource 属性来定义受保护的资源。不同的是过滤器安全拦截器将URL资源与权限关联,而方法安全拦截器将业务方法与权限关联。
你猜对了,我们要做的就是自定义这个 objectDefinitionSource 的实现,首先让我们来认识一下系统为我们提供的ObjectDefinitionSource接口 ,objectDefinitionSource属性正是指向此接口的实现类。该接口中定义了3 个方法,ConfigAttributeDefinition getAttributes(Object object) 方法用户获取保护资源对应的权限信息,该方法返回一个ConfigAttributeDefinition对象(位于org.springframework.security 包下),通过源代码我们可以知道,该对象中实际就只有一个List列表,我们可以通过使用ConfigAttributeDefinition类的构造函数来创建这个List列表,这样,安全拦截器就通过调用getAttributes(Object object) 方法来获取ConfigAttributeDefinition对象,并将该对象和当前用户拥有的Authentication 对象传递给accessDecisionManager (访问决策管理器,请查看org.springframework.security.vote 包下的AffirmativeBased 类,该类是访问决策管理器的一个实现类,它通过一组投票者来决定用户是否有访问当前请求资源的权限),访问决策管理器在将其传递给AffirmativeBased类维护的投票者,这些投票者从ConfigAttributeDefinition对象中获取这个存放了访问保护资源需要的权限信息的列表,然后遍历这个列表并与Authentication 对象中GrantedAuthority[] 数据中的用户权限信息进行匹配,如果匹配成功,投票者就会投赞成票,否则就投反对票,最后访问决策管理器来统计这些投票决定用户是否能访问该资源。是不是又觉得乱了,还是那句话,如果你结合源代码你现在一定更明白了。