Spring Security 梳理 - DelegatingFilterProxy
阿新 • • 發佈:2018-12-01
可能你會覺得奇怪,我們在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">
<bean class="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所寫)
(注:原創文章,轉載請註明出處。原文地址:http://elim.iteye.com/blog/2161648)