现在的位置: 首页 > 综合 > 正文

Spring Security学习总结二

2018年05月20日 ⁄ 综合 ⁄ 共 16814字 ⁄ 字号 评论关闭

背景知识Spring
Security 学习总结一

              SpringSide(你可以去官网了解更多信息,我也是从这里开始了解和学习Spring Security的)

前一篇文章里介绍了Spring Security的一些基础知识,相信你对Spring Security的工作流程已经有了一定的了解,如果你同时在读源代码,那你应该可以认识的更深刻。在这篇文章里,我们将对Spring Security进行一些自定义的扩展,比如自定义实现UserDetailsService,保护业务方法以及如何对用户权限等信息进行动态的配置管理。

说明:
如果你通过Google搜索,可以找到很 多类似主题的文章,本文的目的在于通过这些实例来介绍的工作
原理,我觉得这些才是最重要的。相信你在读完本文之后应该可以按照自己的想法去扩展Spring Secu
rity,这也是我写这两篇文章的目的。希望能对 初学者们有所帮助。
 

废话少说,咱们直接进入正题。

一 自定义UserDetailsService实现

UserDetailsService接口,这个接口中只定义了唯一的UserDetails loadUserByUsername(String username)方法,它通过用户名来获取整个UserDetails对象。

前一篇文章已经介绍了系统提供的默认实现方式InMemoryDaoImpl,它从配置文件中读取用户的身份信息(用户名,密码等),如果你的客户想修改用户信息,就需要直接修改配置文件(你需要告诉用户配置文件的路径,应该在什么地方修改,如何把明文密码通过MD5加密以及如何重启服务器等)。听起来是不是很费劲啊!

在实际应用中,我们可能需要提供动态的方式来获取用户身份信息,最常用的莫过于数据库了,当然也可以是LDAP服务器等。本文首先介绍系统提供的一个默认实现类JdbcDaoImplorg.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相关资料)例,MappingSqlQuerymapRow()方法将一个ResultSet(结果集)中的字段映射一个,Spring
Security
为我们提供了默认的数据库表,如下图所示(摘自《Spring in Action》)

1 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                truetruenew 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,truenew GrantedAuthority[]{new 
29                                                GrantedAuthorityImpl("HOLDER")});
30 
31            user.setEmail(email);
32 
33            user.setUserId(userId);
34 
35            return user;
36 
37        }
38 
39 }

如果你已经看过源代码,你会发现这里只是其中的一部分代码 ,具体的实现请看项目的MySecurityJdbcDaoImpl类实现,以及MyUserDetailsMyUser类,这里步在一一列出。

如果使用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的知识不是本文介绍的内容。到这里保护业务方法的配置就介绍完了。

将资源放在数据库中

现在,你的用户提出了新的需求,它们需要自己可以给系统用户分配或者取消权限。其实这个并不是什么新鲜事,作为开发者,你也应该为用户提供这样的功能。那么我们就需要这些受保护的资源和用户权限等信息都是动态的,你可以选择把它们存放在数据库中或者LDAP服务器上,本文以数据库为例,介绍如何实现用户权限的动态控制。

通过前面的介绍,你可能也注意到了,不管是MethodSecurityInterceptor还是FilterSecurityInterceptor都使用authenticationManageraccessDecisionManager属性用于验证用户,并且都是通过使用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[]数据中的用户权限信息进行匹配,如果匹配成功,投票者就会投赞成票,否则就投反对票,最后访问决策管理器来统计这些投票决定用户是否能访问该资源。是不是又觉得乱了,还是那句话,如果你结合源代码你现在一定更明白了。

说了这么些,那我们到底应该如何来实现这个ObjectDefinitionSource接口呢?

首先还是说说Acegi Seucrity 1.x版本,org.acegisecurity.intercept.weborg.acegisecurity.intercept.method包下AbstractFilterInvocationDefinitionSourceAbstractMethodDefinitionSource两个抽象类,这两个类分别实现了FilterInvocationDefinitionSourceMethodDefinitionSource接口,而这两个接口都继承自ObjectDefinitionSource接口并实现了其中的方法。两个抽象类都使用方法模板模式来实现,将具体的实现方法交给了子类。

提示:
两个抽象类实现了各自接口的 getAttributes(Object object)方法并在此方法中调用lookupAttributes
(Method method)
方法,而实际该方法在抽象类中并没有具体的实现,而是留给了子类去实现。
 

在Acegi Seucrity 1.x版本中,系统为我们提供了默认的实现,MethodDefinitionMap类用于返回方法的权限信息,而PathBasedFilterInvocationDefinitionMap类和RegExpBasedFilterInvocationDefinitionMap类用于返回URL资源对应的权限信息,也就是ConfigAttributeDefinition对象,现在也许明白一点儿了吧,我们只要按照这三个类的实现方式(也就是”模仿”,从后面的代码中你可以看到)从数据库中获取用户信息和权限信息然后封装成一个ConfigAttributeDefinition对象返回即可(其实就是一个List列表,前面已经介绍过了),相信通过Hibernate从数据库中获取一个列表应该是再容易不过的了。

回到Spring Security,系统为我们提供的默认实现有些变化,DefaultFilterInvocationDefinitionSourceDelegatingMethodDefinitionSource两个类,从名字也可以看出来它们分别是干什么的了。这两个类分别实现了FilterInvocationDefinitionSourceMethodDefinitionSource接口,而这两个接口都继承自ObjectDefinitionSource接口并实现了其中的方法,这和1.x版本中一样。它们都是从配置文件中得到资源和相应权限的信息。

通过上面的介绍,你或许更名白了一些,那我们下面要做的就是实现系统的FilterInvocationDefinitionSourceMethodDefinitionSource接口,只是数据源不是从配置文件中读取配置信息是数据库而已。

我们这里对比着Acegi Seucrity 1.x版本中的实现,我个人认为它更好理解,还是请你好好看看源代码。

自定义FilterInvocationDefinitionSource

在2.0中,系统没有在系统抽象类,所以我们还是使用1.x中的实现方式,首先通过一个抽象类来实现ObjectDefinitionSource接口。代码如下:

 1 public ConfigAttributeDefinition getAttributes(Object object)
 2 
 3            throws IllegalArgumentException {
 4 
 5        if (object == null || !(this.supports(object.getClass()))) {
 6 
 7            thrownew IllegalArgumentException("Object must be a FilterInvocation");
 8 
 9        }
10 
11        String url = ((FilterInvocation)object).getRequestUrl();
12 
13        returnthis.lookupAttributes(url);
14 
15     }
16 
17 publicabstract ConfigAttributeDefinition lookupAttributes(String url);
18 
19 @SuppressWarnings("unchecked")
20 
21 publicabstract Collection getConfigAttributeDefinitions();
22 
23 @SuppressWarnings("unchecked")
24 
25 publicboolean supports(Class clazz) {
26 
27     return FilterInvocation.class.isAssignableFrom(clazz);
28 
29 }

这段代码你也可以在1.0中找到,getAttributes方法的入口参数是一个Object对象,这是由系统传给我们的,因为是URL资源的请求,所有可以将这个Object对象强制转换为FilterInvocation对象,并通过调用它的getRequestUrl()方法来获取用户当前请求的URL地址,然后调用子类需要实现的lookupAttributes方法并将该URL地址作为参数传给该方法,下面是具体的实现类DataBaseFilterInvocationDefinitionSource类的代码,也就是我们需要实现抽象父类的lookupAttributes方法:


  1 @Override
  2 
  3 public ConfigAttributeDefinition lookupAttributes(String url) {
  4 
  5        // TODO Auto-generated method stub
  6 
  7        //初始化数据,从数据库读取
  8 
  9     cacheManager.initResourceInCache();
 10 
 11     if (isUseAntPath()) {
 12 
 13        int firstQuestionMarkIndex = url.lastIndexOf("?");
 14 
 15        if (firstQuestionMarkIndex != -1) {
 16 
 17            url = url.substring(0, firstQuestionMarkIndex);
 18 
 19        }
 20 
 21     }
 22 
 23     //将URL在比较前都转换为小写
 24 
 25     if (isConvertUrlToLowercaseBeforeComprison()) {
 26 
 27        url = url.toLowerCase();
 28 
 29     }
 30 
 31     //获取所有的URL
 32 
 33     List<String> urls = cacheManager.getUrlResources();
 34 
 35     //倒叙排序--如果不进行排序,如果用户使用浏览器的导航工具访问页面可能出现问题
 36 
 37     //例如:访问被拒绝后用户刷新页面
 38 
 39     Collections.sort(urls);
 40 
 41     Collections.reverse(urls);
 42 
 43     GrantedAuthority[] authorities = new GrantedAuthority[0];
 44 
 45     //将请求的URL与配置的URL资源进行匹配,并将正确匹配的URL资源对应的权限
 46 
 47     //取出
 48 
 49     for (String resourceName_url : urls) {
 50 
 51        boolean matched = false;
 52 
 53        //使用ant匹配URL
 54 
 55        if (isUseAntPath()) {
 56 
 57            matched = pathMatcher.match(resourceName_url, url);
 58 
 59        } else {//perl5编译URL
 60 
 61            Pattern compliedPattern = null;
 62 
 63            Perl5Compiler compiler = new Perl5Compiler();
 64 
 65            try {
 66 
 67               compliedPattern = compiler.compile(resourceName_url, Perl5Compiler.READ_ONLY_MASK);
 68 
 69            } catch (MalformedPatternException e) {
 70 
 71               e.printStackTrace();
 72 
 73            }
 74 
 75            matched = matcher.matches(url, compliedPattern);
 76 
 77        }
 78 
 79        //匹配正确,获取响应权限
 80 
 81        if (matched) {
 82 
 83            //获取正确匹配URL资源对应的权限
 84 
 85            ResourcDetail detail = cacheManager.getResourcDetailFromCache(resourceName_url);
 86 
 87            authorities = detail.getAuthorities();
 88 
 89            break;
 90 
 91        }
 92 
 93 }
 94 
 95        //将权限封装成ConfigAttributeDefinition对象返回(使用ConfigAttributeEditor)
 96 
 97        if (authorities.length > 0) {
 98 
 99            String authTemp = "";
100 
101            for (GrantedAuthority grantedAuthority : authorities) {
102 
103               authTemp += grantedAuthority.getAuthority() + ",";
104 
105            }
106 
107            String authority = authTemp.substring(0, (authTemp.length() - 1));
108 
109            System.out.println(authority);
110 
111            ConfigAttributeEditor attributeEditor = new ConfigAttributeEditor();
112 
113            attributeEditor.setAsText(authority.trim());
114 
115            return (ConfigAttributeDefinition)attributeEditor.getValue();
116 
117        }
118 
119        returnnull;
120 
121  }

我们这里同样使用了缓存,它参考自系统的UseCache接口的实现,这里不在介绍,你可以查看本例的源代码和系统的实现和本例的配置文件。这里将用户请求的URL地址与从数据库中获取的受保护的URL资源使用ant和perl5匹配(这取决与你的配置),如果匹配成功则从缓存中获取访问该资源需要的权限信息,并将其封装成ConfigAttributeDefinition对象返回,这里使用org.springframework.security.ConfigAttributeEditor类,该类提供了一个setAsText(String
s)
,该方法收取一个字符串作为参数,在该方法中创建ConfigAttributeDefinition对象并将字符串参数传递给ConfigAttributeDefinition类的构造函数来初始化该对象。详细的实现还是请你看源代码。现在我们在配置文件添加自己的实现,如下:

1 <bean id="objectDefinitionSource"
2      class="org.security.intercept.web.DataBaseFilterInvocationDefinitionSource"
3      p:convertUrlToLowercaseBeforeComprison="true"
4      p:useAntPath="true"
5      p:cacheManager-ref="securityCacheManager"/>

    convertUrlToLowercaseBeforeComprison属性定义了在匹配之前将URL都转换为小写,useAntPath属性定义使用Ant方式匹配URL,cacheManager属性定义了指向另一个Bean的引用,我们使用它从缓存中获取相应的信息。

自定义MethodDefinitionSource

将方法资源存放在数据库中的实现与URL资源类似,这里不在累述,下面是DataBaseMethodInvocationDefinitionSource源代码,读者可以参考注释进行阅读(该类也是继承自一个自定义的抽象类AbstractMethodDefinitionSource):

 1 public ConfigAttributeDefinition lookupAttributes(Method method, Class targetClass) {
 2 
 3        // TODO Auto-generated method stub
 4 
 5        //初始化资源并缓存
 6 
 7        securityCacheManager.initResourceInCache();
 8 
 9        //获取所有方法资源
10 
11        List<String> methods = securityCacheManager.getMethodResources();
12 
13        //权限集合
14 
15        Set<GrantedAuthority> authSet = new HashSet<GrantedAuthority>();
16 
17        //遍历方法资源,并获取匹配的资源名称,然后从缓存中获取匹配正确
18 
19        //的资源对应的权限(ResourcDetail对象的GrantedAuthority[]对象数据)
20 
21        for (String resourceName_method : methods) {
22 
23            if (isMatch(targetClass, method, resourceName_method)) {
24 
25               ResourcDetail detail = securityCacheManager.getResourcDetailFromCache(resourceName_method);
26 
27               if (detail == null) {
28 
29                   break;
30 
31               }
32 
33               GrantedAuthority[] authorities = detail.getAuthorities();
34 
35               if (authorities == null || authorities.length == 0) {
36 
37                   break;
38 
39               }
40 
41               authSet.addAll(Arrays.asList(authorities));
42 
43            }
44 
45        }
46 
47        if (authSet.size() > 0) {
48 
49            String authString = "";
50 
51            for (GrantedAuthority grantedAuthority : authSet) {
52 
53               authString += grantedAuthority.getAuthority() + ",";
54 
55            }
56 
57            String authority = authString.substring(0, (authString.length() - 1));
58 
59            System.out.println(">>>>>>>>>>>>>>>" + authority);
60 
61            ConfigAttributeEditor attributeEditor = new ConfigAttributeEditor();
62 
63            attributeEditor.setAsText(authority.trim());
64 
65            return (ConfigAttributeDefinition)attributeEditor.getValue();
66 
67        }
68 
69        returnnull;
70 
71 }

    isMatch方法用于对用户当前调用的方法与受保护的方法进行匹配,与URL资源类似,请参考代码。下面是applicationContext-security.xml文件中的配置,请查看该配置文件。

1 <bean id="methodDefinitionSource"
2     class="org.security.intercept.method.DataBaseMethodInvocationDefinitionSource"
3     p:securityCacheManager-ref="securityCacheManager"/>

securityCacheManager属性定义了指向另一个Bean的引用,我们使用它从缓存中获取相应的信息。这个Bean和前一节中介绍的一样。只是这里我们获取的是方法保护定义资源。

    本文到此也结束了,还请各位多指教。

    附件:springsecurity2.rar

抱歉!评论已关闭.