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