目錄
1.1 Filter順序
1.2 添加Filter到FilterChain
1.3 DelegatingFilterProxy
1.4 FilterChainProxy
1.5 Spring Security定義好的核心Filter
1.5.1 FilterSecurityInterceptor
1.5.2 ExceptionTranslationFilter
1.5.3 SecurityContextPersistenceFilter
1.5.4 UsernamePasswordAuthenticationFilter
Spring Security的底層是通過一系列的Filter來管理的,每個Filter都有其自身的功能,而且各個Filter在功能上還有關聯關系,所以它們的順序也是非常重要的。
1.1 Filter順序
Spring Security已經定義了一些Filter,不管實際應用中你用到了哪些,它們應當保持如下順序。
(1)ChannelProcessingFilter,如果你訪問的channel錯了,那首先就會在channel之間進行跳轉,如http變為https。
(2)SecurityContextPersistenceFilter,這樣的話在一開始進行request的時候就可以在SecurityContextHolder中建立一個SecurityContext,然后在請求結束的時候,任何對SecurityContext的改變都可以被copy到HttpSession。
(3)ConcurrentSessionFilter,因為它需要使用SecurityContextHolder的功能,而且更新對應session的最后更新時間,以及通過SessionRegistry獲取當前的SessionInformation以檢查當前的session是否已經過期,過期則會調用LogoutHandler。
(4)認證處理機制,如UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等,以至於SecurityContextHolder可以被更新為包含一個有效的Authentication請求。
(5)SecurityContextHolderAwareRequestFilter,它將會把HttpServletRequest封裝成一個繼承自HttpServletRequestWrapper的SecurityContextHolderAwareRequestWrapper,同時使用SecurityContext實現了HttpServletRequest中與安全相關的方法。
(6)JaasApiIntegrationFilter,如果SecurityContextHolder中擁有的Authentication是一個JaasAuthenticationToken,那么該Filter將使用包含在JaasAuthenticationToken中的Subject繼續執行FilterChain。
(7)RememberMeAuthenticationFilter,如果之前的認證處理機制沒有更新SecurityContextHolder,並且用戶請求包含了一個Remember-Me對應的cookie,那么一個對應的Authentication將會設給SecurityContextHolder。
(8)AnonymousAuthenticationFilter,如果之前的認證機制都沒有更新SecurityContextHolder擁有的Authentication,那么一個AnonymousAuthenticationToken將會設給SecurityContextHolder。
(9)ExceptionTransactionFilter,用於處理在FilterChain范圍內拋出的AccessDeniedException和AuthenticationException,並把它們轉換為對應的Http錯誤碼返回或者對應的頁面。
(10)FilterSecurityInterceptor,保護Web URI,並且在訪問被拒絕時拋出異常。
1.2 添加Filter到FilterChain
當我們在使用NameSpace時,Spring Security是會自動為我們建立對應的FilterChain以及其中的Filter。但有時我們可能需要添加我們自己的Filter到FilterChain,又或者是因為某些特性需要自己顯示的定義Spring Security已經為我們提供好的Filter,然后再把它們添加到FilterChain。使用NameSpace時添加Filter到FilterChain是通過http元素下的custom-filter元素來定義的。定義custom-filter時需要我們通過ref屬性指定其對應關聯的是哪個Filter,此外還需要通過position、before或者after指定該Filter放置的位置。誠如在上一節《Filter順序》中所提到的那樣,Spring Security對FilterChain中Filter順序是有嚴格的規定的。Spring Security對那些內置的Filter都指定了一個別名,同時指定了它們的位置。我們在定義custom-filter的position、before和after時使用的值就是對應着這些別名所處的位置。如position=”CAS_FILTER”就表示將定義的Filter放在CAS_FILTER對應的那個位置,before=”CAS_FILTER”就表示將定義的Filter放在CAS_FILTER之前,after=”CAS_FILTER”就表示將定義的Filter放在CAS_FILTER之后。此外還有兩個特殊的位置可以指定,FIRST和LAST,分別對應第一個和最后一個Filter,如你想把定義好的Filter放在最后,則可以使用after=”LAST”。
接下來我們來看一下Spring Security給我們定義好的FilterChain中Filter對應的位置順序、它們的別名以及將觸發自動添加到FilterChain的元素或屬性定義。下面的定義是按順序的。
別名 |
Filter類 |
對應元素或屬性 |
CHANNEL_FILTER |
ChannelProcessingFilter |
http/intercept-url@requires-channel |
SECURITY_CONTEXT_FILTER |
SecurityContextPersistenceFilter |
http |
CONCURRENT_SESSION_FILTER |
ConcurrentSessionFilter |
http/session-management/concurrency-control |
LOGOUT_FILTER |
LogoutFilter |
http/logout |
X509_FILTER |
X509AuthenticationFilter |
http/x509 |
PRE_AUTH_FILTER |
AstractPreAuthenticatedProcessingFilter 的子類 |
無 |
CAS_FILTER |
CasAuthenticationFilter |
無 |
FORM_LOGIN_FILTER |
UsernamePasswordAuthenticationFilter |
http/form-login |
BASIC_AUTH_FILTER |
BasicAuthenticationFilter |
http/http-basic |
SERVLET_API_SUPPORT_FILTER |
SecurityContextHolderAwareRequestFilter |
http@servlet-api-provision |
JAAS_API_SUPPORT_FILTER |
JaasApiIntegrationFilter |
http@jaas-api-provision |
REMEMBER_ME_FILTER |
RememberMeAuthenticationFilter |
http/remember-me |
ANONYMOUS_FILTER |
AnonymousAuthenticationFilter |
http/anonymous |
SESSION_MANAGEMENT_FILTER |
SessionManagementFilter |
http/session-management |
EXCEPTION_TRANSLATION_FILTER |
ExceptionTranslationFilter |
http |
FILTER_SECURITY_INTERCEPTOR |
FilterSecurityInterceptor |
http |
SWITCH_USER_FILTER |
SwitchUserFilter |
無 |
1.3 DelegatingFilterProxy
可能你會覺得奇怪,我們在web應用中使用Spring Security時只在web.xml文件中定義了如下這樣一個Filter,為什么你會說是一系列的Filter呢?
<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>
而且如果你不在web.xml文件聲明要使用的Filter,那么Servlet容器將不會發現它們,它們又怎么發生作用呢?這就是上述配置中DelegatingFilterProxy的作用了。
DelegatingFilterProxy是Spring中定義的一個Filter實現類,其作用是代理真正的Filter實現類,也就是說在調用DelegatingFilterProxy的doFilter()方法時實際上調用的是其代理Filter的doFilter()方法。其代理Filter必須是一個Spring bean對象,所以使用DelegatingFilterProxy的好處就是其代理Filter類可以使用Spring的依賴注入機制方便自由的使用ApplicationContext中的bean。那么DelegatingFilterProxy如何知道其所代理的Filter是哪個呢?這是通過其自身的一個叫targetBeanName的屬性來確定的,通過該名稱,DelegatingFilterProxy可以從WebApplicationContext中獲取指定的bean作為代理對象。該屬性可以通過在web.xml中定義DelegatingFilterProxy時通過init-param來指定,如果未指定的話將默認取其在web.xml中聲明時定義的名稱。
<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>
在上述配置中,DelegatingFilterProxy代理的就是名為SpringSecurityFilterChain的Filter。
需要注意的是被代理的Filter的初始化方法init()和銷毀方法destroy()默認是不會被執行的。通過設置DelegatingFilterProxy的targetFilterLifecycle屬性為true,可以使被代理Filter與DelegatingFilterProxy具有同樣的生命周期。
1.4 FilterChainProxy
Spring Security底層是通過一系列的Filter來工作的,每個Filter都有其各自的功能,而且各個Filter之間還有關聯關系,所以它們的組合順序也是非常重要的。
使用Spring Security時,DelegatingFilterProxy代理的就是一個FilterChainProxy。一個FilterChainProxy中可以包含有多個FilterChain,但是某個請求只會對應一個FilterChain,而一個FilterChain中又可以包含有多個Filter。當我們使用基於Spring Security的NameSpace進行配置時,系統會自動為我們注冊一個名為springSecurityFilterChain類型為FilterChainProxy的bean(這也是為什么我們在使用SpringSecurity時需要在web.xml中聲明一個name為springSecurityFilterChain類型為DelegatingFilterProxy的Filter了。),而且每一個http元素的定義都將擁有自己的FilterChain,而FilterChain中所擁有的Filter則會根據定義的服務自動增減。所以我們不需要顯示的再定義這些Filter對應的bean了,除非你想實現自己的邏輯,又或者你想定義的某個屬性NameSpace沒有提供對應支持等。
Spring security允許我們在配置文件中配置多個http元素,以針對不同形式的URL使用不同的安全控制。Spring Security將會為每一個http元素創建對應的FilterChain,同時按照它們的聲明順序加入到FilterChainProxy。所以當我們同時定義多個http元素時要確保將更具有特性的URL配置在前。
<security:http pattern="/login*.jsp*" security="none"/>
<!-- http元素的pattern屬性指定當前的http對應的FilterChain將匹配哪些URL,如未指定將匹配所有的請求 -->
<security:http pattern="/admin/**">
<security:intercept-url pattern="/**" access="ROLE_ADMIN"/>
</security:http>
<security:http>
<security:intercept-url pattern="/**" access="ROLE_USER"/>
</security:http>
需要注意的是http擁有一個匹配URL的pattern,未指定時表示匹配所有的請求,其下的子元素intercept-url也有一個匹配URL的pattern,該pattern是在http元素對應pattern基礎上的,也就是說一個請求必須先滿足http對應的pattern才有可能滿足其下intercept-url對應的pattern。
1.5 Spring Security定義好的核心Filter
通過前面的介紹我們知道Spring Security是通過Filter來工作的,為保證Spring Security的順利運行,其內部實現了一系列的Filter。這其中有幾個是在使用Spring Security的Web應用中必定會用到的。接下來我們來簡要的介紹一下FilterSecurityInterceptor、ExceptionTranslationFilter、SecurityContextPersistenceFilter和UsernamePasswordAuthenticationFilter。在我們使用http元素時前三者會自動添加到對應的FilterChain中,當我們使用了form-login元素時UsernamePasswordAuthenticationFilter也會自動添加到FilterChain中。所以我們在利用custom-filter往FilterChain中添加自己定義的這些Filter時需要注意它們的位置。
1.5.1 FilterSecurityInterceptor
FilterSecurityInterceptor是用於保護Http資源的,它需要一個AccessDecisionManager和一個AuthenticationManager的引用。它會從SecurityContextHolder獲取Authentication,然后通過SecurityMetadataSource可以得知當前請求是否在請求受保護的資源。對於請求那些受保護的資源,如果Authentication.isAuthenticated()返回false或者FilterSecurityInterceptor的alwaysReauthenticate屬性為true,那么將會使用其引用的AuthenticationManager再認證一次,認證之后再使用認證后的Authentication替換SecurityContextHolder中擁有的那個。然后就是利用AccessDecisionManager進行權限的檢查。
我們在使用基於NameSpace的配置時所配置的intercept-url就會跟FilterChain內部的FilterSecurityInterceptor綁定。如果要自己定義FilterSecurityInterceptor對應的bean,那么該bean定義大致如下所示:
<bean id="filterSecurityInterceptor"
class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
<property name="securityMetadataSource">
<security:filter-security-metadata-source>
<security:intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN" />
</security:filter-security-metadata-source>
</property>
</bean>
filter-security-metadata-source用於配置其securityMetadataSource屬性。intercept-url用於配置需要攔截的URL與對應的權限關系。
1.5.2 ExceptionTranslationFilter
通過前面的介紹我們知道在Spring Security的Filter鏈表中ExceptionTranslationFilter就放在FilterSecurityInterceptor的前面。而ExceptionTranslationFilter是捕獲來自FilterChain的異常,並對這些異常做處理。ExceptionTranslationFilter能夠捕獲來自FilterChain所有的異常,但是它只會處理兩類異常,AuthenticationException和AccessDeniedException,其它的異常它會繼續拋出。如果捕獲到的是AuthenticationException,那么將會使用其對應的AuthenticationEntryPoint的commence()處理。如果捕獲的異常是一個AccessDeniedException,那么將視當前訪問的用戶是否已經登錄認證做不同的處理,如果未登錄,則會使用關聯的AuthenticationEntryPoint的commence()方法進行處理,否則將使用關聯的AccessDeniedHandler的handle()方法進行處理。
AuthenticationEntryPoint是在用戶沒有登錄時用於引導用戶進行登錄認證的,在實際應用中應根據具體的認證機制選擇對應的AuthenticationEntryPoint。
AccessDeniedHandler用於在用戶已經登錄了,但是訪問了其自身沒有權限的資源時做出對應的處理。ExceptionTranslationFilter擁有的AccessDeniedHandler默認是AccessDeniedHandlerImpl,其會返回一個403錯誤碼到客戶端。我們可以通過顯示的配置AccessDeniedHandlerImpl,同時給其指定一個errorPage使其可以返回對應的錯誤頁面。當然我們也可以實現自己的AccessDeniedHandler。
<bean id="exceptionTranslationFilter"
class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<beanclass="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp" />
</bean>
</property>
<property name="accessDeniedHandler">
<bean class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
<property name="errorPage" value="/access_denied.jsp" />
</bean>
</property>
</bean>
在上述配置中我們指定了AccessDeniedHandler為AccessDeniedHandlerImpl,同時為其指定了errorPage,這樣發生AccessDeniedException后將轉到對應的errorPage上。指定了AuthenticationEntryPoint為使用表單登錄的LoginUrlAuthenticationEntryPoint。此外,需要注意的是如果該filter是作為自定義filter加入到由NameSpace自動建立的FilterChain中時需把它放在內置的ExceptionTranslationFilter后面,否則異常都將被內置的ExceptionTranslationFilter所捕獲。
<security:http>
<security:form-login login-page="/login.jsp"
username-parameter="username" password-parameter="password"
login-processing-url="/login.do" />
<!-- 退出登錄時刪除session對應的cookie -->
<security:logout delete-cookies="JSESSIONID" />
<!-- 登錄頁面應當是不需要認證的 -->
<security:intercept-url pattern="/login*.jsp*"
access="IS_AUTHENTICATED_ANONYMOUSLY" />
<security:intercept-url pattern="/**" access="ROLE_USER" />
<security:custom-filter ref="exceptionTranslationFilter"after="EXCEPTION_TRANSLATION_FILTER"/>
</security:http>
在捕獲到AuthenticationException之后,調用AuthenticationEntryPoint的commence()方法引導用戶登錄之前,ExceptionTranslationFilter還做了一件事,那就是使用RequestCache將當前HttpServletRequest的信息保存起來,以至於用戶成功登錄后需要跳轉到之前的頁面時可以獲取到這些信息,然后繼續之前的請求,比如用戶可能在未登錄的情況下發表評論,待用戶提交評論的時候就會將包含評論信息的當前請求保存起來,同時引導用戶進行登錄認證,待用戶成功登錄后再利用原來的request包含的信息繼續之前的請求,即繼續提交評論,所以待用戶登錄成功后我們通常看到的是用戶成功提交了評論之后的頁面。Spring Security默認使用的RequestCache是HttpSessionRequestCache,其會將HttpServletRequest相關信息封裝為一個SavedRequest保存在HttpSession中。
1.5.3 SecurityContextPersistenceFilter
SecurityContextPersistenceFilter會在請求開始時從配置好的SecurityContextRepository中獲取SecurityContext,然后把它設置給SecurityContextHolder。在請求完成后將SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository,同時清除SecurityContextHolder所持有的SecurityContext。在使用NameSpace時,Spring Security默認會給SecurityContextPersistenceFilter的SecurityContextRepository設置一個HttpSessionSecurityContextRepository,其會將SecurityContext保存在HttpSession中。此外HttpSessionSecurityContextRepository有一個很重要的屬性allowSessionCreation,默認為true。這樣需要把SecurityContext保存在session中時,如果不存在session,可以自動創建一個。也可以把它設置為false,這樣在請求結束后如果沒有可用的session就不會保存SecurityContext到session了。SecurityContextRepository還有一個空實現,NullSecurityContextRepository,如果在請求完成后不想保存SecurityContext也可以使用它。
這里再補充說明一點為什么SecurityContextPersistenceFilter在請求完成后需要清除SecurityContextHolder的SecurityContext。SecurityContextHolder在設置和保存SecurityContext都是使用的靜態方法,具體操作是由其所持有的SecurityContextHolderStrategy完成的。默認使用的是基於線程變量的實現,即SecurityContext是存放在ThreadLocal里面的,這樣各個獨立的請求都將擁有自己的SecurityContext。在請求完成后清除SecurityContextHolder中的SucurityContext就是清除ThreadLocal,Servlet容器一般都有自己的線程池,這可以避免Servlet容器下一次分發線程時線程中還包含SecurityContext變量,從而引起不必要的錯誤。
下面是一個SecurityContextPersistenceFilter的簡單配置。
<bean id="securityContextPersistenceFilter"
class="org.springframework.security.web.context.SecurityContextPersistenceFilter">
<property name='securityContextRepository'>
<bean
class='org.springframework.security.web.context.HttpSessionSecurityContextRepository'>
<property name='allowSessionCreation' value='false' />
</bean>
</property>
</bean>
1.5.4 UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter用於處理來自表單提交的認證。該表單必須提供對應的用戶名和密碼,對應的參數名默認為j_username和j_password。如果不想使用默認的參數名,可以通過UsernamePasswordAuthenticationFilter的usernameParameter和passwordParameter進行指定。表單的提交路徑默認是“j_spring_security_check”,也可以通過UsernamePasswordAuthenticationFilter的filterProcessesUrl進行指定。通過屬性postOnly可以指定只允許登錄表單進行post請求,默認是true。其內部還有登錄成功或失敗后進行處理的AuthenticationSuccessHandler和AuthenticationFailureHandler,這些都可以根據需求做相關改變。此外,它還需要一個AuthenticationManager的引用進行認證,這個是沒有默認配置的。
<bean id="authenticationFilter"
class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="usernameParameter" value="username"/>
<property name="passwordParameter" value="password"/>
<property name="filterProcessesUrl" value="/login.do" />
</bean>
如果要在http元素定義中使用上述AuthenticationFilter定義,那么完整的配置應該類似於如下這樣子。
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:security="http://www.springframework.org/schema/security"
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-3.1.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<!-- entry-point-ref指定登錄入口 -->
<security:http entry-point-ref="authEntryPoint">
<security:logout delete-cookies="JSESSIONID" />
<security:intercept-url pattern="/login*.jsp*"
access="IS_AUTHENTICATED_ANONYMOUSLY" />
<security:intercept-url pattern="/**" access="ROLE_USER" />
<!-- 添加自己定義的AuthenticationFilter到FilterChain的FORM_LOGIN_FILTER位置 -->
<security:custom-filter ref="authenticationFilter" position="FORM_LOGIN_FILTER"/>
</security:http>
<!-- AuthenticationEntryPoint,引導用戶進行登錄 -->
<bean id="authEntryPoint"class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>
<!-- 認證過濾器 -->
<bean id="authenticationFilter"
class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="usernameParameter" value="username"/>
<property name="passwordParameter" value="password"/>
<property name="filterProcessesUrl" value="/login.do" />
</bean>
<security:authentication-manager alias="authenticationManager">
<security:authentication-provider
user-service-ref="userDetailsService">
<security:password-encoder hash="md5"
base64="true">
<security:salt-source user-property="username" />
</security:password-encoder>
</security:authentication-provider>
</security:authentication-manager>
<bean id="userDetailsService"
class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl">
<property name="dataSource" ref="dataSource" />
</bean>
</beans>
(注:本文是基於Spring Security3.1.6所寫)