Spring Security的使用

1.Spring Security简要介绍

Spring Security以前叫做acegi,是后来才成为Spring的一个子项目,也是目前最为流行的一个安全权限管理框架,它与Spring紧密结合在一起。

Spring Security关注的重点是在企业应用安全层为您提供服务,你将发现业务问题领域存在着各式各样的需求。银行系统跟电子商务应用就有很大的不同。电子商务系统与企业销售自动化工具又有很大不同。这些客户化需求让应用安全显得有趣,富有挑战性而且物有所值。Spring Security为基于J2EE的企业应用软件提供了一套全面的安全解决方案。

2.为Spring Security配置过滤器和其他参数

要使用Spring Security,首先就是在web.xml中为它配置过滤器, 其次因为我的spring配置文件是放在WEB-INF下的,因此还要配置上下文的参数,最后添加spring的监听器:

view plaincopy to clipboardprint?
<?xml version="1.0" encoding="UTF-8"?>  
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee   http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">  
    <!-- 配置上下文参数,指定spring配置文件的位置 -->  
    <context-param>  
        <param-name>contextConfigLocation</param-name>  
        <param-value>/WEB-INF/spring-*.xml</param-value>  
    </context-param>  
    <!-- spring security必须的过滤器,保证在访问所有的页面时都必须通过认证 -->  
    <filter>  
        <filter-name>springSecurityFilterChain</filter-name>  
        <filter-class>  
            org.springframework.web.filter.DelegatingFilterProxy   
        </filter-class>  
    </filter>  
    <filter-mapping>  
        <filter-name>springSecurityFilterChain</filter-name>  
        <url-pattern>/*</url-pattern>  
    </filter-mapping>  
    <listener>  
        <listener-class>  
            org.springframework.web.context.ContextLoaderListener   
        </listener-class>  
    </listener>  
    <welcome-file-list>  
        <welcome-file>index.jsp</welcome-file>  
    </welcome-file-list>  
    <login-config>  
        <auth-method>BASIC</auth-method>  
    </login-config>  
</web-app>  

3.配置security(spring-security.xml)

view plaincopy to clipboardprint?
<?xml version="1.0" encoding="UTF-8"?>  
<!-- 这里必须使用security的命名空间,提供了beans这个假名 -->  
<beans:beans xmlns="http://www.springframework.org/schema/security"  
    xmlns:beans="http://www.springframework.org/schema/beans"  
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"  
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd   
                        http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">  
  
    <!-- Spring Security采用就近原则,有多个约束时,从上至下只要找到第一条满足就返回,因此因该将最严格的约束放在最前面,而将最宽松的约束放在最后面.auto-config属性可以让spring security为我们自动配置几种常用的权限控制机制,包括form,anonymous, rememberMe等。当然你也可以手工配置。-->  
    <http auto-config="true">  
        <!-- 我们利用intercept-url来判断用户需要具有何种权限才能访问对应的url资源,可以在pattern中指定一个特定的url资源,也可以使用通配符指定一组类似的url资源。例子中定义的两个intercepter-url,第一个用来控制对/security/**的访问,第二个使用了通配符/**,说明它将控制对系统中所有url资源的访问。 -->  
        <intercept-url pattern="/security/**" access="ROLE_ADMIN" />  
        <intercept-url pattern="/**" access="ROLE_ADMIN,ROLE_USER" />  
        <intercept-url pattern="/login.jsp*" filters="none" />  
        <logout logout-url="/logout.jsp"  
            logout-success-url="/j_spring_security_check" />  
    </http>  
  
    <!-- 使用内存权限管理的配置信息, 在tomcat启动时,会加载这个文件并一直保存在内存中,知道应用程序重启,所以也叫内存权限管理   
        <authentication-provider>  
        <user-service>  
        <user name="admin" password="tomcat" authorities="ROLE_ADMIN"/>  
        <user name="liky" password="redhat" authorities="ROLE_USER"/>        
        </user-service>  
        </authentication-provider>  
    -->  
    <!-- 使用数据库作为权限管理的来源,data-source-ref指定了数据源,所指定的数据源必须包含users, authorities表,并符合security的定义规范 -->  
    <authentication-provider>  
        <jdbc-user-service data-source-ref="dataSource" />  
    </authentication-provider>  
  
</beans:beans>  

4.数据源的配置(spring-common.xml)


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

 <!-- 定义数据源 -->
 <bean id="dataSource"
  class="org.apache.commons.dbcp.BasicDataSource">
  <property name="driverClassName"
   value="com.mysql.jdbc.Driver">
  </property>
  <property name="url" value="jdbc:mysql://localhost:3306/csu"></property>
  <property name="username" value="root"></property>
  <property name="password" value="redhat"></property>
  <property name="maxActive" value="100"></property>
  <property name="maxIdle" value="30"></property>
  <property name="maxWait" value="300"></property>
  <property name="defaultAutoCommit" value="true"></property>
 </bean> 
</beans>

5.项目的目录结构

6. 数据库脚本


/-- 注意这里的脚本是MYSQL的,因此在你演示这个实例的时候,要加入MySQL的驱动包 --/   
    
create table users   
(   
username varchar(50) primary key,   
password varchar(50),   
enabled tinyint(1)   
);   
  
create table authorities   
(   
id int auto_increment primary key,   
username varchar(50),   
authority varchar(50),   
constraint fk_authorities_users foreign key(username) references users(username)   
);   
  
create unique index ix_auth_username on authorities (username,authority);  


7.部署和配置的要点说明

这是一个Spring Security的数据库认证实例,要注意以下几点:
(1)请自行加入Spring必须的包,Spring security的包和MySQL的驱动包,当然你也可以换成其他的数据库,但是你要相应的修改spring-common.xml(PS:应该也是指ApplicationContext)中的dataSource部分
(2)数据库中的两个表users,authorites必须完全按照脚本所示来定义,也就是说表的名字不能修改.
(3)users表必须包含username,password,enabled字段,这三个字段是绝对不能少的,也不能修改类型.另外enabled一定要为1才能登录
(4)authorities表必须包含username字段,这个字段引用users的username作为外键,authority字段就是角色的名字,角色名字必须满足ROLE_XXX的格式(例如:ROLE_ADMIN,ROLE_USER,ROLE_MAMAGER)
(5)如果一个用户有多个角色,不要将多个角色放在一起用逗号隔开.而是每个角色定义一条记录(例如:abu有ROLE_ADMIN,ROLE_USER两个角色,那么应该定义两条记录: 一条为abu, ROLE_USER,另一条为abu, ROLE_ADMIN.而不是只有一条:abu, ROLE_ADMIN,ROLE_USER)
(6)你可以给authorities表添加一个id字段作为主键.



二 保护Web资源

    Spring Security提供了很多的过滤器,它们拦截Servlet请求,并将这些请求转交给认证处理过滤器和访问决策过滤器进行处理,并强制安全性,认证用户 身份和用户权限以达到保护Web资源的目的。对于Web资源我们大约可以只用6个过滤器来保护我们的应用系统,下表列出了这些安全过滤器的名称作用以及它 们在系统中的执行顺序:

  

               

通道处理过滤器

确保请求是在安全通道(HTTP和HTTPS)之上传输的

认证处理过滤器

接受认证请求,并将它们转交给认证管理器进行身份验证

CAS 处理过滤器

接受CAS服务票据,验证Yale CAS(单点登录)是否已经对用户进行了认证

HTTP 基本授权过滤器

处理使用HTTP基本认证的身份验证请求

集成过滤器

处理认证信息在请求间的存储 (比如在HTTP会话中)

安全强制过滤器

确保用户己经认证,并且满足访问一个受保护Web资源的权限需求

    接下来,通过一个实例来说明它们的具体使用方法和如何在Spring中进行配置。

    1 建立Spring Security项目

    首先在MyEclipse中创建一个Web Project,并使用MyEclipse工具导入Spring项目的依赖JAR包,并生成默认的,这里暂时不会用到这个文件,本文只是通过一个简单的实 例来说明如何配置使用Spring Security,不会涉及到数据库,而是使用一个用户属性(users.properties)文件来保存用户信息(包括用户名,密码及相应的权限), 但在实际的项目中,我们很少会这样做,而是应该把用户信息存在数据库中,下一篇文章中将会详细介绍并用到这个文件来配置Hibernate,这里我们保留 它。

    现在还需要为项目导入Spring Security的JAR包,它没有包括在Spring Framework中,你可以从http://www.springframework.org/download/ 下载 , 并将spring-security-core-2.0.0.jar(这是核心代码库)和spring-security-core-tiger- 2.0.0.jar(和annotation有关的,比如使用注解对方法进行安全访问控制,在下一篇中会用到)拷贝到项目的lib目录下,其中也包括两个 实例(tutorial和contacts),并且两个实例中都包括了如何使用Spring 2.0的命名空间来配置Spring Security,无论你对Spring 2.0命名空间的使用是否了解,它将使我们的配置文件大大缩短,简化开发,提高生产效率。到此,我们的Spring Security项目就建好了,项目目录结构如下图所示:

Spring Security的使用_第1张图片

    图2 项目目录结构

    2 配置web.xml

    Spring Security使用一组过滤器链来对用户进行身份验证和授权。首先,在web.xml文件中添加FilterToBeanProxy过滤器配置:

1  <  filter  >     
2       <  filter-name  > springSecurityFilterChain  </  filter-name  > 
3       <  filter-class  > 
4         org.springframework.security.util.FilterToBeanProxy
5       </  filter-class  > 
6       <  init-param  >         
7         <
  param-name  > targetClass  </  param-name  > 
8           <  param-value  >             
9             org.springframework.security.util.FilterChainProxy
10           </  param-value  > 
11        </  init-param  > 
12  </  filter  > 
13

    org.springframework.security.util.FilterToBeanProxy实现了Filter接口,它通过调用 WebapplicationContextUtils类的getWebApplicationnContext(servletContext)方法来 获取Spring的应用上下文句柄,并通过getBean(beanName)方法来获取Spring受管Bean的对象,即这里targetClass 参数配置的Bean,并通过调用FilterChain Proxy的init()方法来启动Spring Security过滤器链进行各种身份验证和授权服务(FilterChainProxy类也是实现了Filter接口),从而将过滤功能委托给 Spring的FilterChainProxy受管Bean(它维护着一个处理验证和授权的过滤器列表,列表中的过滤器按照一定的顺序执行并完成认证过 程),这样即简化了web.xml文件的配置,又能充分利用 Spring的IoC功能来完成这些过滤器执行所需要的其它资源的注入。

    当用户发出请求,过滤器需要根据web.xml配置的请求映射地址来拦截用户请求,这时Spring Security开始工作,它会验证你的身份以及当前请求的资源是否与你拥有的权限相符,从而达到保护Web资源的功能,下面是本例所要过滤的用户请求地址:

< filter-mapping > 
2 
3         < filter-name > springSecurityFilterChain </ filter-name > 
4 
5         < url-pattern > / j_spring_security_check </ url-pattern > 
6 
7      </ filter-mapping > 
8 
9      < filter-mapping > 
10 
11         < filter-name > springSecurityFilterChain </ filter-name > 
12 
13         < url-pattern > /* </ url-pattern > 
14 
15 </ filter-mapping >

提示:
/j_spring_security_check是Spring Security默认的进行表单验证的过滤地址,你也可以修改为别的名称,但是需要和
applicationContext-security.xml中相对应,当然还会涉及到其它一些默认值(可能是一个成员变量,也可能是别的请
求地址),在下文我们将看到,建议你在阅读此文的同时,应该参照Spring Security项目的源代码,便于你更好的理解。
 

3 配置applicationContext-security.xml

    3.1 FilterChainProxy过滤器链

    FilterChainProxy会按顺序来调用一组filter,使这些filter即能完成验证授权的本质工作,又能享用Spring Ioc的功能来方便的得到其它依赖的资源。FilterChainProxy配置如下:

1  <  bean  id  ="filterChainProxy"      
        class
  ="org.springframework.security.util.FilterChainProxy"  > 
2        <  property  name  ="filterInvocationDefinitionSource"  > 
3           <  value  >  <![CDATA[            
                CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON 
4               PATTERN_TYPE_APACHE_ANT          
                /**=httpSessionContextIntegrationFilter,logoutFilter,
5               authenticationProcessingFilter,securityContextHolderAwareRequestFilter,
6               rememberMeProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,
7               filterSecurityInterceptor 
8            ]]>  </  value  > 
9        </  property  > 
10  </  bean  >

    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON 定义URL在匹配之前必须先转为小写,PATTERN_TYPE_APACHE_ANT 定义了使用Apache ant的匹配模式,/**定义的将等号后面的过滤器应用在那些URL上,这里使用全部URL过滤,每个过滤器之间都适用逗号分隔,它们按照一定的顺序排 列。

提示: 
特别需要注意的是,即使你配置了系统提供的所有过滤器,这个过滤器链会很长,但是千万不要使用换行,否则它们不会正常工作,
容器甚至不能正常启动。
 

    下面根据FilterChainProxy的配置来介绍各个过滤器的配置,各个过滤器的执行顺序如以上配置。

    首先是通道处理过滤器,如果你需要使用HTTPS,这里我们就使用HTTP进行传输,所以不需要配置通道处理过滤器,然后是集成过滤器,配置如下:

1  <  bean  id  ="httpSessionContextIntegrationFilter" 
2 
3  class  ="org.springframework.security.context.HttpSessionContextIntegrationFilter"  />

    httpSessionContextIntegrationFilter是集成过滤器的一个实现,在用户的一个请求过程中,用户的认证信息通过 SecurityContextHolder(使用ThreadLoacl实现)进行传递的,所有的过滤器都是通过 SecurityContextHolder来获取用户的认证信息,从而在一次请求中所有过滤器都能共享Authentication(认证),减少了 HttpRequest参数的传送,下面的代码是从安全上下文的获取Authentication对象的方法:

1 SecurityContext context = SecurityContextHolder.getContext();
2 
3 Authentication authentication = context.getAuthentication();

    但是,ThreadLoacl不能跨越多个请求存在,所以,集成过滤器在请求开始时从Http会话中取出用户认证信息并创建一个 SecurityContextHolder将Authentication对象保存在其中,在请求结束之后,在从 SecurityContextHolder中获取Authentication对象并将其放回Http会话中,共下次请求使用,从而达到了跨越多个请求 的目的。集成过滤器还有其它的实现,可以参考相关文档。

提示:
集成过滤器必须在其它过滤器之前被使用。
 

    logoutFilter(退出过滤器) ,退出登录操作:

1  <  bean  id  ="logoutFilter" 
2 
3       class  ="org.springframework.security.ui.logout.LogoutFilter"  > 
4 
5       <  constructor-arg  value  ="/index.jsp"  /> 
6 
7       <  constructor-arg  > 
8 
9          <  list  > 
10 
11              <!--  实现了LogoutHandler接口(logout方法)  --> 
12 
13              <  ref  bean  ="rememberMeServices"  /> 
14 
15              <  bean  class  ="org.springframework.security.ui.logout.SecurityContextLogoutHandler"  /> 
16 
17          </  list  > 
18 
19       </  constructor-arg  > 
20 
21  </  bean  >

 

LogoutFilter的构造函数需要两个参数,第一个是退出系统后系统跳转到的URL,第二个是一个LogoutHandler类型的数组,这个数组 里的对象都实现了LogoutHandler接口,并实现了它的logout方法,用户在发送退出请求后,会一次执行LogoutHandler数组的对 象并调用它们的 logout方法进行一些后续的清理操作,主要是从SecurityContextHolder对象中清楚所有用户的认证信息 (Authentication对象),将用户的会话对象设为无效,这些都时由SecurityContextLogoutHandler来完成。 LogoutFilter还会清除Cookie记录,它由另外一个Bean来完成(RememberMeServices)。

    <ref bean="rememberMeServices"/>标记指向了我们另外配置的一个Bean:

1  <  bean  id  ="rememberMeServices"       
        class
  ="org.springframework.security.ui.rememberme.TokenBasedRememberMeServices" 
2       p:key  ="springsecurity" 
3      p:userDetailsService-ref  ="userDetailsService"  />

    TokenBasedRememberMeServices继承自系统的AbstractRememberMeServices抽象类(实现了 RememberMeServices和 LogoutHandler两个接口), RememberMeServices接口的loginSuccess方法负责在用户成功登录之后将用户的认证信息存入Cookie中,这个类在后续的过 滤器执行过程中也会被用到。

    另一个userDetailsService属性也是指向了我们配置的Bean, 它负责从数据库中读取用户的信息,这个类的详细配置将在后面的部分详细介绍,这里只是简单的认识一下。

    过滤器链的下个配置的过滤器是authenticationProcessingFilter(认证过程过滤器),我们使用它来处理表单认证,当接受到与filterProcessesUrl所定义相同的请求时它开始工作:

1  <  bean  id  ="authenticationProcessingFilter" 
2 
3       class  ="org.springframework.security.ui.webapp.AuthenticationProcessingFilter" 
4 
5       p:authenticationManager-ref  ="authenticationManager" 
6       p:authenticationFailureUrl  ="/login.jsp?login_error=1" 
7      p:defaultTargetUrl  ="/default.jsp" 
8      p:filterProcessesUrl  ="/j_spring_security_check" 
9      p:rememberMeServices-ref  ="rememberMeServices"  />

    下面列出了认证过程过滤器配置中各个属性的功能:

    1.authenticationManager     认证管理器

    2.authenticationFailureUrl 定义登录失败时转向的页面

    3.defaultTargetUrl         定义登录成功时转向的页面

    4.filterProcessesUrl        定义登录请求的地址(在web.xml中配置过)

    5.rememberMeServices        在验证成功后添加cookie信息

    这里也用到了rememberMeServices,如果用户认证成功,将调用RememberMeServices的loginSuccess方法将用户认证信息写入Cookie中,这里也可以看到使用IoC的好处。

    决定用户是否有权限访问受保护资源的第一步就是要确定用户的身份,最常用的方式就是用户提供一个用户名和密码以确认用户的身份是否合法,这一步就是由认证 过程过滤器调用authenticationManager(认证管理器)来完成的。 org.springframework.security.AuthenticationManager接口定义了一个authenticate方法, 它使用Authentication作为入口参数(只包含用户名和密码),并在验证成功后返回一个完整的Authentication对象(包含用户的权 限信息GrantedAuthority数组对象),authenticationProcessingFilter(认证过程过滤器)会将这个完整的 Authentication对象存入SecurityContext中,如果认证失败会抛出一个AuthenticationException并跳转 到authenticationFailureUrl 定义的URL.认证管理其配置如下:

1  <  bean  id  ="authenticationManager" 
2 
3  class  ="org.springframework.security.providers.ProviderManager" 
4  p:sessionController-ref  ="concurrentSessionController"  > 
5       <  property  name  ="providers"  > 
6          <  list  > 
7              <  ref  bean  ="daoAuthenticationProvider"  /> 
8              <  bean
9 
10  class  ="org.springframework.security.providers.anonymous.AnonymousAuthenticationProvider" 
11                 p:key  ="springsecurity"  /> 
12              <  bean
13 
14  class  ="org.springframework.security.providers.rememberme.RememberMeAuthenticationProvider" 
15                 p:key  ="springsecurity"  /> 
16          </  list  > 
17       </  property  >   
18  </  bean  >


<  filter-mapping  > 
2 
3          <  filter-name  > springSecurityFilterChain  </  filter-name  > 
4 
5          <  url-pattern  > /j_spring_security_check  </  url-pattern  > 
6 
7       </  filter-mapping  > 
8 
9       <  filter-mapping  > 
10 
11          <  filter-name  > springSecurityFilterChain  </  filter-name  > 
12 
13          <  url-pattern  > /*  </  url-pattern  > 
14 
15  </  filter-mapping  >

提示:
/j_spring_security_check是Spring Security默认的进行表单验证的过滤地址,你也可以修改为别的名称,但是需要和
applicationContext-security.xml中相对应,当然还会涉及到其它一些默认值(可能是一个成员变量,也可能是别的请
求地址),在下文我们将看到,建议你在阅读此文的同时,应该参照Spring Security项目的源代码,便于你更好的理解。
 

3 配置applicationContext-security.xml

    3.1 FilterChainProxy过滤器链

    FilterChainProxy会按顺序来调用一组filter,使这些filter即能完成验证授权的本质工作,又能享用Spring Ioc的功能来方便的得到其它依赖的资源。FilterChainProxy配置如下:

1  <  bean  id  ="filterChainProxy"      
        class
  ="org.springframework.security.util.FilterChainProxy"  > 
2        <  property  name  ="filterInvocationDefinitionSource"  > 
3           <  value  >  <![CDATA[            
                CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON 
4               PATTERN_TYPE_APACHE_ANT          
                /**=httpSessionContextIntegrationFilter,logoutFilter,
5               authenticationProcessingFilter,securityContextHolderAwareRequestFilter,
6               rememberMeProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,
7               filterSecurityInterceptor 
8            ]]>  </  value  > 
9        </  property  > 
10  </  bean  >

    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON 定义URL在匹配之前必须先转为小写,PATTERN_TYPE_APACHE_ANT 定义了使用Apache ant的匹配模式,/**定义的将等号后面的过滤器应用在那些URL上,这里使用全部URL过滤,每个过滤器之间都适用逗号分隔,它们按照一定的顺序排 列。

提示: 
特别需要注意的是,即使你配置了系统提供的所有过滤器,这个过滤器链会很长,但是千万不要使用换行,否则它们不会正常工作,
容器甚至不能正常启动。
 

    下面根据FilterChainProxy的配置来介绍各个过滤器的配置,各个过滤器的执行顺序如以上配置。

    首先是通道处理过滤器,如果你需要使用HTTPS,这里我们就使用HTTP进行传输,所以不需要配置通道处理过滤器,然后是集成过滤器,配置如下:

1  <  bean  id  ="httpSessionContextIntegrationFilter" 
2 
3  class  ="org.springframework.security.context.HttpSessionContextIntegrationFilter"  />

    httpSessionContextIntegrationFilter是集成过滤器的一个实现,在用户的一个请求过程中,用户的认证信息通过 SecurityContextHolder(使用ThreadLoacl实现)进行传递的,所有的过滤器都是通过 SecurityContextHolder来获取用户的认证信息,从而在一次请求中所有过滤器都能共享Authentication(认证),减少了 HttpRequest参数的传送,下面的代码是从安全上下文的获取Authentication对象的方法:

1 SecurityContext context = SecurityContextHolder.getContext();
2 
3 Authentication authentication = context.getAuthentication();

    但是,ThreadLoacl不能跨越多个请求存在,所以,集成过滤器在请求开始时从Http会话中取出用户认证信息并创建一个 SecurityContextHolder将Authentication对象保存在其中,在请求结束之后,在从 SecurityContextHolder中获取Authentication对象并将其放回Http会话中,共下次请求使用,从而达到了跨越多个请求 的目的。集成过滤器还有其它的实现,可以参考相关文档。

提示:
集成过滤器必须在其它过滤器之前被使用。
 

    logoutFilter(退出过滤器) ,退出登录操作:

1  <  bean  id  ="logoutFilter" 
2 
3       class  ="org.springframework.security.ui.logout.LogoutFilter"  > 
4 
5       <  constructor-arg  value  ="/index.jsp"  /> 
6 
7       <  constructor-arg  > 
8 
9          <  list  > 
10 
11              <!--  实现了LogoutHandler接口(logout方法)  --> 
12 
13              <  ref  bean  ="rememberMeServices"  /> 
14 
15              <  bean  class  ="org.springframework.security.ui.logout.SecurityContextLogoutHandler"  /> 
16 
17          </  list  > 
18 
19       </  constructor-arg  > 
20 
21  </  bean  >

 

LogoutFilter的构造函数需要两个参数,第一个是退出系统后系统跳转到的URL,第二个是一个LogoutHandler类型的数组,这个数组 里的对象都实现了LogoutHandler接口,并实现了它的logout方法,用户在发送退出请求后,会一次执行LogoutHandler数组的对 象并调用它们的 logout方法进行一些后续的清理操作,主要是从SecurityContextHolder对象中清楚所有用户的认证信息 (Authentication对象),将用户的会话对象设为无效,这些都时由SecurityContextLogoutHandler来完成。 LogoutFilter还会清除Cookie记录,它由另外一个Bean来完成(RememberMeServices)。

    <ref bean="rememberMeServices"/>标记指向了我们另外配置的一个Bean:

1  <  bean  id  ="rememberMeServices"       
        class
  ="org.springframework.security.ui.rememberme.TokenBasedRememberMeServices" 
2       p:key  ="springsecurity" 
3      p:userDetailsService-ref  ="userDetailsService"  />

    TokenBasedRememberMeServices继承自系统的AbstractRememberMeServices抽象类(实现了 RememberMeServices和 LogoutHandler两个接口), RememberMeServices接口的loginSuccess方法负责在用户成功登录之后将用户的认证信息存入Cookie中,这个类在后续的过 滤器执行过程中也会被用到。

    另一个userDetailsService属性也是指向了我们配置的Bean, 它负责从数据库中读取用户的信息,这个类的详细配置将在后面的部分详细介绍,这里只是简单的认识一下。

    过滤器链的下个配置的过滤器是authenticationProcessingFilter(认证过程过滤器),我们使用它来处理表单认证,当接受到与filterProcessesUrl所定义相同的请求时它开始工作:

1  <  bean  id  ="authenticationProcessingFilter" 
2 
3       class  ="org.springframework.security.ui.webapp.AuthenticationProcessingFilter" 
4 
5       p:authenticationManager-ref  ="authenticationManager" 
6       p:authenticationFailureUrl  ="/login.jsp?login_error=1" 
7      p:defaultTargetUrl  ="/default.jsp" 
8      p:filterProcessesUrl  ="/j_spring_security_check" 
9      p:rememberMeServices-ref  ="rememberMeServices"  />

    下面列出了认证过程过滤器配置中各个属性的功能:

    1.authenticationManager     认证管理器

    2.authenticationFailureUrl 定义登录失败时转向的页面

    3.defaultTargetUrl         定义登录成功时转向的页面

    4.filterProcessesUrl        定义登录请求的地址(在web.xml中配置过)

    5.rememberMeServices        在验证成功后添加cookie信息

    这里也用到了rememberMeServices,如果用户认证成功,将调用RememberMeServices的loginSuccess方法将用户认证信息写入Cookie中,这里也可以看到使用IoC的好处。

    决定用户是否有权限访问受保护资源的第一步就是要确定用户的身份,最常用的方式就是用户提供一个用户名和密码以确认用户的身份是否合法,这一步就是由认证 过程过滤器调用authenticationManager(认证管理器)来完成的。 org.springframework.security.AuthenticationManager接口定义了一个authenticate方法, 它使用Authentication作为入口参数(只包含用户名和密码),并在验证成功后返回一个完整的Authentication对象(包含用户的权 限信息GrantedAuthority数组对象),authenticationProcessingFilter(认证过程过滤器)会将这个完整的 Authentication对象存入SecurityContext中,如果认证失败会抛出一个AuthenticationException并跳转 到authenticationFailureUrl 定义的URL.认证管理其配置如下:

1  <  bean  id  ="authenticationManager" 
2 
3  class  ="org.springframework.security.providers.ProviderManager" 
4  p:sessionController-ref  ="concurrentSessionController"  > 
5        <  property  name  ="providers"  > 
6           <  list  > 
7               <  ref  bean  ="daoAuthenticationProvider"  /> 
8               <  bean
9 
10  class  ="org.springframework.security.providers.anonymous.AnonymousAuthenticationProvider" 
11                 p:key  ="springsecurity"  /> 
12               <  bean
13 
14  class  ="org.springframework.security.providers.rememberme.RememberMeAuthenticationProvider" 
15                 p:key  ="springsecurity"  /> 
16           </  list  > 
17        </  property  >    
18  </  bean  >





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

    一 自定义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接口,因为从数据库中读取信息,所以首先需要一个数据 源对象,这里不在多说,这里需要重点介绍的是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类的部分代码:

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

四将资源放在数据库中

    现在,你的用户提出了新的需求,它们需要自己可以给系统用户分配或者取消权限。其实这个并不是什么新鲜事,作为开发者,你也应该为用户提供这样的功能。那 么我们就需要这些受保护的资源和用户权限等信息都是动态的,你可以选择把它们存放在数据库中或者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[]数据中的用户权限信息进行 匹配,如果匹配成功,投票者就会投赞成票,否则就投反对票,最后访问决策管理器来统计这些投票决定用户是否能访问该资源。是不是又觉得乱了,还是那句话, 如果你结合源代码你现在一定更明白了。

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

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

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

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

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

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


2 自定义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和前一节中介绍的一样。只是这里我们获取的是方法保护定义资源。



本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/csuliky/archive/2009/06/17/4277413.aspx


你可能感兴趣的:(Spring Security的使用)