Spring Security 名稱空間背後

工程 | Luke Taylor | 2010 年 3 月 6 日 | ...

隨著 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 時代經歷過的人來說,是時候重溫一下了。

Spring Bean 版本

認證 Bean

首先讓我們看一下 <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 配置中。

Web Bean

現在我們來看看如何展開 <http> 塊。這更復雜,因為建立的 bean 與名稱空間中使用的元素名稱沒有那麼明顯的對應關係。

這個FilterChainProxy

正如你可能已經知道的,Spring Security 的 Web 功能是使用 servlet 過濾器實現的。它在應用程式上下文中維護自己的過濾器鏈,並使用 Spring 的一個例項將其委託出去DelegatingFilterProxy,在 web.xml 檔案中定義。實現此委託過濾器鏈(或可能多個鏈)的類稱為FilterChainProxy。你可以將 <http> 塊視為建立了FilterChainProxy bean. FilterChainProxy有一個名為filterChainMap的屬性,它是一個將模式對映到過濾器 bean 列表的 Map。例如,你可能有類似如下的配置

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


<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.

SecurityContextPersistenceFilter

此過濾器必須包含在任何過濾器鏈中。它負責在請求之間儲存認證資訊(一個SecurityContext例項)。它還設定了在請求期間儲存它的執行緒區域性變數,並在請求完成時清除它。其預設策略是儲存SecurityContext在 HTTP 會話中,因此使用了HttpSessionSecurityContextRepositorybean。

訪問控制
FilterSecurityInterceptor位於棧的末端,並將配置的安全限制應用於傳入請求。如果請求未被授權(無論是因為使用者未認證,還是因為他們沒有所需的許可權),它將丟擲異常。這將由ExceptionTranslationFilter處理,它會發送使用者一個訪問拒絕訊息,或者透過呼叫配置的AuthenticationEntryPoint來啟動認證過程。在這種情況下,使用了一個LoginUrlAuthenticationEntryPoint,它會將使用者重定向到登入頁面。在此之前,ExceptionTranslationFilter將快取當前請求資訊,以便在認證後(如果需要)可以恢復該資訊。
認證過程
UsernamePasswordAuthenticationFilter負責處理提交的登入表單(它由 <form-login> 名稱空間元素建立)。此 bean 已配置了一個SavedRequestAwareAuthenticationSuccessHandler,這意味著它會將使用者重定向到他們在被要求認證之前最初請求的 URL。原始請求隨後由RequestCacheFilter透過使用請求包裝器來恢復,允許使用者從他們離開的地方繼續。
其他雜項過濾器

LogoutFilter僅負責處理登出連結(/j_spring_security_logout預設為此),清除安全上下文並使會話失效。AnonymousAuthenticationFilter負責為匿名使用者填充安全上下文,從而更容易應用對某些 URL 放寬的預設安全限制。例如,在上述配置中,IS_AUTHENTICATED_ANONYMOUSLY屬性意味著匿名使用者可以訪問登入頁面(但不能訪問其他內容)。檢視手冊中關於此內容的章節以獲取更多資訊。它的使用是可選的,你可以刪除額外的AnonymousAuthenticationProvider如果你不使用它。

SecurityContextHolderAwareRequestFilter提供了標準的 servlet API 安全方法,它使用一個訪問 SecurityContext 的請求包裝器。如果你不需要這些方法,可以省略此過濾器。SessionManagementFilter負責在使用者在當前請求期間進行認證時(例如,透過記住我認證)應用會話相關的策略。在其預設配置中,它會建立一個新的會話(複製現有會話的屬性),目的是更改會話識別符號,並提供針對會話固定攻擊的防禦。當使用 Spring Security 的併發會話控制時,也會用到它。在此配置中,UsernamePasswordAuthenticationFilter是唯一就位的認證機制,並且也注入了一個SessionFixationProtectionStrategy。這意味著我們可以安全地移除會話管理過濾器。

這個AccessDecisionManager

如果你一直仔細留意,你會注意到我們仍然缺少上述配置中的一個 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>

WebInvocationPrivilegeEvaluator

這是另一個由名稱空間註冊的 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 以及當然還有原始碼本身中查詢更多資訊了。

獲取 Spring 新聞通訊

訂閱 Spring 新聞通訊,保持聯絡

訂閱

領先一步

VMware 提供培訓和認證,助你加速前進。

瞭解更多

獲取支援

Tanzu Spring 透過一個簡單的訂閱提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位制檔案。

瞭解更多

即將到來的活動

檢視 Spring 社群所有即將到來的活動。

檢視全部