Spring Security 入門(1-6-2)Spring Security - 內置的filter順序、自定義filter、http元素和對應的filterChain


Spring Security 的底層是通過一系列的 Filter 來管理的,每個 Filter 都有其自身的功能,而且各個 Filter 在功能上還有關聯關系,所以它們的順序也是非常重要的。

1、Spring Security的內置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,並且在訪問被拒絕時拋出異常。

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

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 具有同樣的生命周期。

4、FilterChainProxy

Spring Security 底層是通過一系列的 Filter 來工作的,每個 Filter 都有其各自的功能,而且各個 Filter 之間還有關聯關系,所以它們的組合順序也是非常重要的。

使用 Spring Security 時,DelegatingFilterProxy 代理的就是一個 FilterChainProxy。

一個 FilterChainProxy 中可以包含有多個 FilterChain,但是某個請求只會對應一個 FilterChain,而一個 FilterChain 中又可以包含有多個 Filter。

當我們使用基於 Spring Security 的 NameSpace 進行配置時,系統會自動為我們注冊一個名為 springSecurityFilterChain 類型為 FilterChainProxy 的 bean,

而且每一個 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。

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 時需要注意它們的位置。

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 與對應的權限關系。

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 中。

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>

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>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM