Spring Security 的底層是通過一系列的 Filter 來管理的,每個 Filter 都有其自身的功能,而且各個 Filter 在功能上還有關(guān)聯(lián)關(guān)系,所以它們的順序也是非常重要的。
Spring Security 已經(jīng)定義了一些 Filter,不管實(shí)際應(yīng)用中你用到了哪些,它們應(yīng)當(dāng)保持如下順序。
當(dāng)我們在使用 NameSpace 時,Spring Security 是會自動為我們建立對應(yīng)的 FilterChain 以及其中的 Filter。但有時我們可能需要添加我們自己的 Filter 到 FilterChain,又或者是因?yàn)槟承┨匦孕枰约猴@示的定義 Spring Security 已經(jīng)為我們提供好的 Filter,然后再把它們添加到 FilterChain。使用 NameSpace 時添加 Filter 到 FilterChain 是通過 http 元素下的 custom-filter 元素來定義的。定義 custom-filter 時需要我們通過 ref 屬性指定其對應(yīng)關(guān)聯(lián)的是哪個 Filter,此外還需要通過 position、before 或者 after 指定該 Filter 放置的位置。誠如在上一節(jié)《Filter 順序》中所提到的那樣,Spring Security 對 FilterChain 中 Filter 順序是有嚴(yán)格的規(guī)定的。Spring Security 對那些內(nèi)置的 Filter 都指定了一個別名,同時指定了它們的位置。我們在定義 custom-filter 的 position、before 和 after 時使用的值就是對應(yīng)著這些別名所處的位置。如 position=”CAS_FILTER” 就表示將定義的 Filter 放在 CAS_FILTER 對應(yīng)的那個位置,before=”CAS_FILTER” 就表示將定義的 Filter 放在 CAS_FILTER 之前,after=”CAS_FILTER” 就表示將定義的 Filter 放在 CAS_FILTER 之后。此外還有兩個特殊的位置可以指定,F(xiàn)IRST 和 LAST,分別對應(yīng)第一個和最后一個 Filter,如你想把定義好的 Filter 放在最后,則可以使用 after=”LAST”。
接下來我們來看一下 Spring Security 給我們定義好的 FilterChain 中 Filter 對應(yīng)的位置順序、它們的別名以及將觸發(fā)自動添加到 FilterChain 的元素或?qū)傩远x。下面的定義是按順序的。
別名 | Filter 類 | 對應(yīng)元素或?qū)傩?/strong> |
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 | 無 |
可能你會覺得奇怪,我們在 web 應(yīng)用中使用 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 容器將不會發(fā)現(xiàn)它們,它們又怎么發(fā)生作用呢?這就是上述配置中 DelegatingFilterProxy 的作用了。
DelegatingFilterProxy 是 Spring 中定義的一個 Filter 實(shí)現(xiàn)類,其作用是代理真正的 Filter 實(shí)現(xiàn)類,也就是說在調(diào)用 DelegatingFilterProxy 的 doFilter() 方法時實(shí)際上調(diào)用的是其代理 Filter 的 doFilter() 方法。其代理 Filter 必須是一個 Spring bean 對象,所以使用 DelegatingFilterProxy 的好處就是其代理 Filter 類可以使用 Spring 的依賴注入機(jī)制方便自由的使用 ApplicationContext 中的 bean。那么 DelegatingFilterProxy 如何知道其所代理的 Filter 是哪個呢?這是通過其自身的一個叫 targetBeanName 的屬性來確定的,通過該名稱,DelegatingFilterProxy 可以從 WebApplicationContext 中獲取指定的 bean 作為代理對象。該屬性可以通過在 web.xml 中定義 DelegatingFilterProxy 時通過 init-param 來指定,如果未指定的話將默認(rèn)取其在 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() 默認(rèn)是不會被執(zhí)行的。通過設(shè)置 DelegatingFilterProxy 的 targetFilterLifecycle 屬性為 true,可以使被代理 Filter 與 DelegatingFilterProxy 具有同樣的生命周期。
Spring Security 底層是通過一系列的 Filter 來工作的,每個 Filter 都有其各自的功能,而且各個 Filter 之間還有關(guān)聯(lián)關(guān)系,所以它們的組合順序也是非常重要的。
使用 Spring Security 時,DelegatingFilterProxy 代理的就是一個 FilterChainProxy。一個 FilterChainProxy 中可以包含有多個 FilterChain,但是某個請求只會對應(yīng)一個 FilterChain,而一個 FilterChain 中又可以包含有多個 Filter。當(dāng)我們使用基于 Spring Security 的 NameSpace 進(jìn)行配置時,系統(tǒng)會自動為我們注冊一個名為 springSecurityFilterChain 類型為 FilterChainProxy 的 bean(這也是為什么我們在使用 SpringSecurity 時需要在 web.xml 中聲明一個 name 為 springSecurityFilterChain 類型為 DelegatingFilterProxy 的 Filter 了。),而且每一個 http 元素的定義都將擁有自己的 FilterChain,而 FilterChain 中所擁有的 Filter 則會根據(jù)定義的服務(wù)自動增減。所以我們不需要顯示的再定義這些 Filter 對應(yīng)的 bean 了,除非你想實(shí)現(xiàn)自己的邏輯,又或者你想定義的某個屬性 NameSpace 沒有提供對應(yīng)支持等。
Spring security 允許我們在配置文件中配置多個 http 元素,以針對不同形式的 URL 使用不同的安全控制。Spring Security 將會為每一個 http 元素創(chuàng)建對應(yīng)的 FilterChain,同時按照它們的聲明順序加入到 FilterChainProxy。所以當(dāng)我們同時定義多個 http 元素時要確保將更具有特性的 URL 配置在前。
<security:http pattern="/login*.jsp*" security="none"/>
<!-- http 元素的 pattern 屬性指定當(dāng)前的 http 對應(yīng)的 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 元素對應(yīng) pattern 基礎(chǔ)上的,也就是說一個請求必須先滿足 http 對應(yīng)的 pattern 才有可能滿足其下 intercept-url 對應(yīng)的 pattern。
通過前面的介紹我們知道 Spring Security 是通過 Filter 來工作的,為保證 Spring Security 的順利運(yùn)行,其內(nèi)部實(shí)現(xiàn)了一系列的 Filter。這其中有幾個是在使用 Spring Security 的 Web 應(yīng)用中必定會用到的。接下來我們來簡要的介紹一下 FilterSecurityInterceptor、ExceptionTranslationFilter、SecurityContextPersistenceFilter 和 UsernamePasswordAuthenticationFilter。在我們使用 http 元素時前三者會自動添加到對應(yīng)的 FilterChain 中,當(dāng)我們使用了 form-login 元素時 UsernamePasswordAuthenticationFilter 也會自動添加到 FilterChain 中。所以我們在利用 custom-filter 往 FilterChain 中添加自己定義的這些 Filter 時需要注意它們的位置。
FilterSecurityInterceptor 是用于保護(hù) Http 資源的,它需要一個 AccessDecisionManager 和一個 AuthenticationManager 的引用。它會從 SecurityContextHolder 獲取 Authentication,然后通過 SecurityMetadataSource 可以得知當(dāng)前請求是否在請求受保護(hù)的資源。對于請求那些受保護(hù)的資源,如果 Authentication.isAuthenticated()返回 false 或者 FilterSecurityInterceptor 的 alwaysReauthenticate 屬性為 true,那么將會使用其引用的 AuthenticationManager 再認(rèn)證一次,認(rèn)證之后再使用認(rèn)證后的 Authentication 替換 SecurityContextHolder 中擁有的那個。然后就是利用 AccessDecisionManager 進(jìn)行權(quán)限的檢查。
我們在使用基于 NameSpace 的配置時所配置的 intercept-url 就會跟 FilterChain 內(nèi)部的 FilterSecurityInterceptor 綁定。如果要自己定義 FilterSecurityInterceptor 對應(yīng)的 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 與對應(yīng)的權(quán)限關(guān)系。
通過前面的介紹我們知道在 Spring Security 的 Filter 鏈表中 ExceptionTranslationFilter 就放在 FilterSecurityInterceptor 的前面。而 ExceptionTranslationFilter 是捕獲來自 FilterChain 的異常,并對這些異常做處理。ExceptionTranslationFilter 能夠捕獲來自 FilterChain 所有的異常,但是它只會處理兩類異常,AuthenticationException 和 AccessDeniedException,其它的異常它會繼續(xù)拋出。如果捕獲到的是 AuthenticationException,那么將會使用其對應(yīng)的 AuthenticationEntryPoint 的 commence()處理。如果捕獲的異常是一個 AccessDeniedException,那么將視當(dāng)前訪問的用戶是否已經(jīng)登錄認(rèn)證做不同的處理,如果未登錄,則會使用關(guān)聯(lián)的 AuthenticationEntryPoint 的 commence()方法進(jìn)行處理,否則將使用關(guān)聯(lián)的 AccessDeniedHandler 的 handle()方法進(jìn)行處理。
AuthenticationEntryPoint 是在用戶沒有登錄時用于引導(dǎo)用戶進(jìn)行登錄認(rèn)證的,在實(shí)際應(yīng)用中應(yīng)根據(jù)具體的認(rèn)證機(jī)制選擇對應(yīng)的 AuthenticationEntryPoint。
AccessDeniedHandler 用于在用戶已經(jīng)登錄了,但是訪問了其自身沒有權(quán)限的資源時做出對應(yīng)的處理。ExceptionTranslationFilter 擁有的 AccessDeniedHandler 默認(rèn)是 AccessDeniedHandlerImpl,其會返回一個 403 錯誤碼到客戶端。我們可以通過顯示的配置 AccessDeniedHandlerImpl,同時給其指定一個 errorPage 使其可以返回對應(yīng)的錯誤頁面。當(dāng)然我們也可以實(shí)現(xiàn)自己的 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,這樣發(fā)生 AccessDeniedException 后將轉(zhuǎn)到對應(yīng)的 errorPage 上。指定了 AuthenticationEntryPoint 為使用表單登錄的 LoginUrlAuthenticationEntryPoint。此外,需要注意的是如果該 filter 是作為自定義 filter 加入到由 NameSpace 自動建立的 FilterChain 中時需把它放在內(nèi)置的 ExceptionTranslationFilter 后面,否則異常都將被內(nèi)置的 ExceptionTranslationFilter 所捕獲。
<security:http>
<security:form-login login-page="/login.jsp"
username-parameter="username" password-parameter="password"
login-processing-url="/login.do" />
<!-- 退出登錄時刪除 session 對應(yīng)的 cookie -->
<security:logout delete-cookies="JSESSIONID" />
<!-- 登錄頁面應(yīng)當(dāng)是不需要認(rèn)證的 -->
<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 之后,調(diào)用 AuthenticationEntryPoint 的 commence() 方法引導(dǎo)用戶登錄之前,ExceptionTranslationFilter 還做了一件事,那就是使用 RequestCache 將當(dāng)前 HttpServletRequest 的信息保存起來,以至于用戶成功登錄后需要跳轉(zhuǎn)到之前的頁面時可以獲取到這些信息,然后繼續(xù)之前的請求,比如用戶可能在未登錄的情況下發(fā)表評論,待用戶提交評論的時候就會將包含評論信息的當(dāng)前請求保存起來,同時引導(dǎo)用戶進(jìn)行登錄認(rèn)證,待用戶成功登錄后再利用原來的 request 包含的信息繼續(xù)之前的請求,即繼續(xù)提交評論,所以待用戶登錄成功后我們通常看到的是用戶成功提交了評論之后的頁面。Spring Security 默認(rèn)使用的 RequestCache 是 HttpSessionRequestCache,其會將 HttpServletRequest 相關(guān)信息封裝為一個 SavedRequest 保存在 HttpSession 中。
SecurityContextPersistenceFilter 會在請求開始時從配置好的 SecurityContextRepository 中獲取 SecurityContext,然后把它設(shè)置給 SecurityContextHolder。在請求完成后將 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同時清除 SecurityContextHolder 所持有的 SecurityContext。在使用 NameSpace 時,Spring Security 默認(rèn)會給 SecurityContextPersistenceFilter 的 SecurityContextRepository 設(shè)置一個 HttpSessionSecurityContextRepository,其會將 SecurityContext 保存在 HttpSession 中。此外 HttpSessionSecurityContextRepository 有一個很重要的屬性 allowSessionCreation,默認(rèn)為 true。這樣需要把 SecurityContext 保存在 session 中時,如果不存在 session,可以自動創(chuàng)建一個。也可以把它設(shè)置為 false,這樣在請求結(jié)束后如果沒有可用的 session 就不會保存 SecurityContext 到 session 了。SecurityContextRepository 還有一個空實(shí)現(xiàn),NullSecurityContextRepository,如果在請求完成后不想保存 SecurityContext 也可以使用它。
這里再補(bǔ)充說明一點(diǎn)為什么 SecurityContextPersistenceFilter 在請求完成后需要清除 SecurityContextHolder 的 SecurityContext。SecurityContextHolder 在設(shè)置和保存 SecurityContext 都是使用的靜態(tài)方法,具體操作是由其所持有的 SecurityContextHolderStrategy 完成的。默認(rèn)使用的是基于線程變量的實(shí)現(xiàn),即 SecurityContext 是存放在 ThreadLocal 里面的,這樣各個獨(dú)立的請求都將擁有自己的 SecurityContext。在請求完成后清除 SecurityContextHolder 中的 SucurityContext 就是清除 ThreadLocal,Servlet 容器一般都有自己的線程池,這可以避免 Servlet 容器下一次分發(fā)線程時線程中還包含 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>
UsernamePasswordAuthenticationFilter 用于處理來自表單提交的認(rèn)證。該表單必須提供對應(yīng)的用戶名和密碼,對應(yīng)的參數(shù)名默認(rèn)為 j_username 和 j_password。如果不想使用默認(rèn)的參數(shù)名,可以通過 UsernamePasswordAuthenticationFilter 的 usernameParameter 和 passwordParameter 進(jìn)行指定。表單的提交路徑默認(rèn)是 “j_spring_security_check”,也可以通過 UsernamePasswordAuthenticationFilter 的 filterProcessesUrl 進(jìn)行指定。通過屬性 postOnly 可以指定只允許登錄表單進(jìn)行 post 請求,默認(rèn)是 true。其內(nèi)部還有登錄成功或失敗后進(jìn)行處理的 AuthenticationSuccessHandler 和 AuthenticationFailureHandler,這些都可以根據(jù)需求做相關(guān)改變。此外,它還需要一個 AuthenticationManager 的引用進(jìn)行認(rèn)證,這個是沒有默認(rèn)配置的。
<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 定義,那么完整的配置應(yīng)該類似于如下這樣子。
<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,引導(dǎo)用戶進(jìn)行登錄 -->
<bean id="authEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.jsp"/>
</bean>
<!-- 認(rèn)證過濾器 -->
<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>
更多建議: