領先一步
VMware 提供培訓和認證,助你加速前進。
瞭解更多隨著 Spring Security 2 中安全模式的引入,啟動並執行一個簡單的安全應用程式變得更加容易。在舊版本中,使用者必須單獨宣告和連線所有實現 bean,這導致 Spring 應用程式上下文檔案龐大而複雜,難以理解和維護。學習曲線相當陡峭,我仍然記得在 2004 年我開始參與該專案(當時的 Acegi Security)時,花了一些時間才完全理解它。積極的一面是,接觸到框架的基本構建塊意味著一旦你設法配置出一個可行的設定,幾乎不可能不對重要的類以及它們如何協同工作有所瞭解。反過來,這些知識使你能夠很好地利用 Spring Security 的最大優勢之一——定製化機會。
現在我們有很多 Spring Security 使用者從一開始就使用了名稱空間,並受益於其提供的簡潔性和快速開發機會,但當你想要超越名稱空間提供的功能時,事情就會變得更加困難。此時,你必須開始理解框架架構以及你的自定義類如何融入其中。你必須知道要擴充套件哪些類、實現哪些策略介面以及將它們插入何處。學習曲線仍然存在,只是位置發生了變化。名稱空間有意地提供了 Spring Security 所解決問題領域的高階檢視,因此它實際上隱藏了實現細節,使得難以瞭解實際情況。它確實提供了很多擴充套件點,但無論出於何種原因,你可能覺得你需要深入研究。
在本文中,我們將探討 Web 應用程式的一個簡單名稱空間配置,以及如果將其完全展開為 Spring bean 配置會是什麼樣子。我們不會深入探討所有 bean 的具體作用,但你可以在參考手冊和 Javadoc 中找到關於特定類和介面的更多資訊。本文內容主要面向已經熟悉基礎知識的現有使用者,因此如果你之前沒有使用過 Spring Security,至少應該閱讀參考手冊中的名稱空間章節,並花一些時間研究示例應用程式。
首先,讓我們看一下我們想要替換的名稱空間配置。
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
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.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<http>
<intercept-url pattern="/secure/extreme/**" access="ROLE_SUPERVISOR" />
<intercept-url pattern="/secure/**" access="IS_AUTHENTICATED_FULLY" />
<intercept-url pattern="/login.htm" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/images/*" filters="none" />
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page="/login.htm" default-target-url="/home.htm" />
<logout logout-success-url="/logged_out.htm" />
</http>
<authentication-manager>
<authentication-provider>
<password-encoder hash="md5"/>
<user-service>
<user name="bob" password="12b141f35d58b8b3a46eea65e6ac179e" authorities="ROLE_SUPERVISOR, ROLE_USER" />
<user name="sam" password="d1a5e26d0558c455d386085fad77d427" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
這是一個相當簡單的例子,類似於你在線上示例和專案附帶的示例應用程式中看到的內容。它定義了一個記憶體中的使用者帳戶列表用於認證,每個使用者都有一個許可權列表(在本例中是簡單的角色)。它還配置了 Web 應用程式中受保護的 URL 模式集、一個基於表單的認證機制以及對基本登出 URL 的支援。
如何用老式的 bean 配置來重現這一點?對於那些在 Acegi Security 時代經歷過的人來說,是時候重溫一下了。
首先讓我們看一下 <authentication-manager> 元素,它(從 Spring Security 3.0 開始)必須在任何基於名稱空間的配置中宣告。在這個例子中,<http> 部分依賴於這個元素(form-login 認證機制使用它來進行認證)。實際的依賴關係在於介面AuthenticationManager,它封裝了 Spring Security 配置提供的認證服務。你可以在這一層提供自己的實現,但大多數人使用預設的ProviderManager,它委託給一個列表AuthenticationProvider例項。配置可能看起來像這樣
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:sec="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.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<bean id="authenticationManager" class="org.springframework.security.authentication.ProviderManager">
<property name="providers">
<list>
<ref bean="authenticationProvider" />
<ref bean="anonymousProvider" />
</list>
</property>
</bean>
<bean id="authenticationProvider" class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
<property name="passwordEncoder">
<bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" />
</property>
<property name="userDetailsService" ref="userService" />
</bean>
<bean id="anonymousProvider" class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="SomeUniqueKeyForThisApplication" />
</bean>
<sec:user-service id="userService">
<sec:user name="bob" password="12b141f35d58b8b3a46eea65e6ac179e" authorities="ROLE_SUPERVISOR, ROLE_USER" />
<sec:user name="sam" password="d1a5e26d0558c455d386085fad77d427" authorities="ROLE_USER" />
</sec:user-service>
</beans>
在此階段,我們保留了 <user-service> 元素,以說明它可以單獨使用來建立一個UserDetailsService例項,並將其注入到DaoAuthenticationProvider中。我們還切換了預設 XML 名稱空間,改用“beans”。從現在起,我們將以此為準。UserDetailsService是框架中的一個重要介面,它只是一個用於獲取使用者資訊的 DAO。它的唯一職責是載入指定使用者帳戶的資料。對應的 bean 配置將是
<bean id="userService" class="org.springframework.security.core.userdetails.memory.InMemoryDaoImpl">
<property name="userMap">
<value>
bob=12b141f35d58b8b3a46eea65e6ac179e,ROLE_SUPERVISOR,ROLE_USER
sam=d1a5e26d0558c455d386085fad77d427,ROLE_USER
</value>
</property>
</bean>
在本例中,名稱空間語法更清晰,但你可能希望使用自己的UserDetailsService實現。Spring Security 也提供了標準的基於 JDBC 和 LDAP 的版本。我們還添加了一個AnonymousAuthenticationProvider,它純粹是為了支援AnonymousAuthenticationFiter,它出現在下面的 Web 配置中。
<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<property name="matcher">
<bean class="org.springframework.security.web.util.AntUrlPathMatcher"/>
</property>
<property name="filterChainMap">
<map>
<entry key="/somepath/**">
<list>
<ref local="filter1"/>
</list>
</entry>
<entry key="/images/*">
<list/>
</entry>
<entry key="/**">
<list>
<ref local="filter1"/>
<ref local="filter2"/>
<ref local="filter3"/>
</list>
</entry>
</map>
</property>
</bean>
其中 filter1、filter2 等是應用程式上下文中實現javax.servlet.Filter介面的其他 bean 的名稱。
所以FilterChainProxy將傳入請求與過濾器列表匹配,並將請求傳遞給它找到的第一個匹配鏈。請注意,除了 "/images/*" 模式(它對映到一個空的過濾器鏈)外,這些與 <intercept-url< 名稱空間元素中的模式沒有關聯。<http> 配置目前只能維護一個對映到所有請求(除了那些配置為完全繞過過濾器鏈的請求)的單一過濾器列表。
由於上述配置有點冗長,因此有一個更緊湊的名稱空間語法可用於配置FilterChainProxymap,且不會損失任何功能。上述配置的等效表示將是
<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<sec:filter-chain-map path-type="ant">
<sec:filter-chain pattern="/somepath/**" filters="filter1"/>
<sec:filter-chain pattern="/images/*" filters="none"/>
<sec:filter-chain pattern="/**" filters="filter1, filter2, filter3"/>
</sec:filter-chain-map>
</bean>
現在過濾器鏈被指定為 bean 名稱的有序列表,按照過濾器應用的順序排列。那麼我們原始的名稱空間配置會建立哪些過濾器呢?在這種情況下,FilterChainProxy將是
<alias name="filterChainProxy" alias="springSecurityFilterChain"/>
<bean id="filterChainProxy" class="org.springframework.security.web.FilterChainProxy">
<sec:filter-chain-map path-type="ant">
<sec:filter-chain pattern="/images/*" filters="none"/>
<sec:filter-chain pattern="/**" filters="securityContextFilter, logoutFilter, formLoginFilter, requestCacheFilter,
servletApiFilter, anonFilter, sessionMgmtFilter, exceptionTranslator, filterSecurityInterceptor" />
</sec:filter-chain-map>
</bean>
所以這裡面有九個過濾器,其中一些是可選的,一些是必需的。此時你可以看到,你現在接觸到了名稱空間為你遮蔽的許多細節。你同時控制使用的過濾器以及它們的呼叫順序,這兩點都至關重要。
我們還為這個 bean 添加了別名,以匹配之前在web.xml中使用的名稱。或者,你也可以直接使用“filterChainProxy”。
現在我們將看看這九個過濾器 bean 以及支援它們所需的其他 bean。
<bean id="securityContextFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter" >
<property name="securityContextRepository" ref="securityContextRepository" />
</bean>
<bean id="securityContextRepository"
class="org.springframework.security.web.context.HttpSessionSecurityContextRepository" />
<bean id="logoutFilter" class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg value="/logged_out.htm" />
<constructor-arg>
<list><bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler" /></list>
</constructor-arg>
</bean>
<bean id="formLoginFilter" class="org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationSuccessHandler">
<bean class="org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<property name="defaultTargetUrl" value="/index.jsp" />
</bean>
</property>
<property name="sessionAuthenticationStrategy">
<bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
</property>
</bean>
<bean id="requestCacheFilter" class="org.springframework.security.web.savedrequest.RequestCacheAwareFilter" />
<bean id="servletApiFilter" class="org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter" />
<bean id="anonFilter" class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter" >
<property name="key" value="SomeUniqueKeyForThisApplication" />
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS" />
</bean>
<bean id="sessionMgmtFilter" class="org.springframework.security.web.session.SessionManagementFilter" >
<constructor-arg ref="securityContextRepository" />
</bean>
<bean id="exceptionTranslator" class="org.springframework.security.web.access.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<property name="loginFormUrl" value="/login.htm"/>
</bean>
</property>
</bean>
<bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="securityMetadataSource">
<sec:filter-security-metadata-source>
<sec:intercept-url pattern="/secure/extreme/*" access="ROLE_SUPERVISOR"/>
<sec:intercept-url pattern="/secure/**" access="IS_AUTHENTICATED_FULLY" />
<sec:intercept-url pattern="/login.htm" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<sec:intercept-url pattern="/**" access="ROLE_USER" />
</sec:filter-security-metadata-source>
</property>
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
</bean>
同樣,我們使用了方便的名稱空間元素,filter-security-metadata-source,用於建立SecurityMetadataSource例項,該例項由FilterSecurityInterceptor使用,但你可以在此處插入自己的 bean(請參閱這個 FAQ 瞭解示例)。filter-security-metadata-source元素建立了一個DefaultFilterInvocationSecurityMetadataSource.
此過濾器必須包含在任何過濾器鏈中。它負責在請求之間儲存認證資訊(一個SecurityContext例項)。它還設定了在請求期間儲存它的執行緒區域性變數,並在請求完成時清除它。其預設策略是儲存SecurityContext在 HTTP 會話中,因此使用了HttpSessionSecurityContextRepositorybean。
LogoutFilter僅負責處理登出連結(/j_spring_security_logout預設為此),清除安全上下文並使會話失效。AnonymousAuthenticationFilter負責為匿名使用者填充安全上下文,從而更容易應用對某些 URL 放寬的預設安全限制。例如,在上述配置中,IS_AUTHENTICATED_ANONYMOUSLY屬性意味著匿名使用者可以訪問登入頁面(但不能訪問其他內容)。檢視手冊中關於此內容的章節以獲取更多資訊。它的使用是可選的,你可以刪除額外的AnonymousAuthenticationProvider如果你不使用它。
SecurityContextHolderAwareRequestFilter提供了標準的 servlet API 安全方法,它使用一個訪問 SecurityContext 的請求包裝器。如果你不需要這些方法,可以省略此過濾器。SessionManagementFilter負責在使用者在當前請求期間進行認證時(例如,透過記住我認證)應用會話相關的策略。在其預設配置中,它會建立一個新的會話(複製現有會話的屬性),目的是更改會話識別符號,並提供針對會話固定攻擊的防禦。當使用 Spring Security 的併發會話控制時,也會用到它。在此配置中,UsernamePasswordAuthenticationFilter是唯一就位的認證機制,並且也注入了一個SessionFixationProtectionStrategy。這意味著我們可以安全地移除會話管理過濾器。
如果你一直仔細留意,你會注意到我們仍然缺少上述配置中的一個 bean 引用。安全攔截器需要配置一個AccessDecisionManager。如果你使用名稱空間,內部會建立一個,但你也可以插入自定義 bean。沒有名稱空間,我們需要顯式提供一個。名稱空間內部版本的等效配置看起來像這樣
<bean id="accessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased">
<property name="decisionVoters">
<list>
<bean class="org.springframework.security.access.vote.RoleVoter"/>
<bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
</list>
</property>
</bean>
這是另一個由名稱空間註冊的 bean,儘管它不是直接必需的(它可能用於一些 JSP 標籤)。它允許你查詢當前使用者是否被允許呼叫特定的 URL。這在你的 controller bean 中可能很有用,用於確定在呈現的檢視中應該提供哪些資訊或導航連結。
<bean id="webPrivilegeEvaluator" class="org.springframework.security.web.access.DefaultWebInvocationPrivilegeEvaluator">
<constructor-arg ref="filterSecurityInterceptor" />
</bean>
再次重申,本文的目的並非詳細解釋所有這些 bean 的工作原理,而是主要提供一個參考,幫助你從基本的名稱空間配置進一步深入,並理解其底層原理。正如你所見,這相當複雜!但請記住,可以將相當多的這些 bean 插入到名稱空間配置本身中,現在你可以看到它們實際的去向。既然你已經知道哪些類涉及其中,你就知道可以在 Spring Security 參考手冊、Javadoc 以及當然還有原始碼本身中查詢更多資訊了。