Spring Security 命名空間幕後解析

工程 | Luke Taylor | March 06, 2010 | ...

隨著 Spring Security 2 中安全性結構描述的引入,建立一個簡單的安全應用程式並使其運作變得容易許多。在舊版本中,使用者必須個別宣告和連接所有實作 Bean,導致 Spring 應用程式 context 檔案龐大而複雜,難以理解和維護。學習曲線相當陡峭,我仍然記得當我開始從事這個專案(當時稱為 Acegi Security)時,在 2004 年,花了一些時間才理解所有內容。從正面來看,這種對框架基本構建模組的接觸意味著,一旦您設法組合出一個可運作的組態,幾乎不可能沒有至少了解一些重要的類別以及它們如何協同工作。這種知識反過來使您處於有利的位置,可以利用自訂的機會,而自訂是使用 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> 部分依賴於此元素(表單登入驗證機制使用它進行驗證)。實際的依賴項是介面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中。我們也切換為使用 "beans" 作為預設的 XML 命名空間。從現在開始,我們將假設這一點。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 篩選器實作的。它在應用程式 context 中維護自己的篩選器鏈,並使用 Spring 的DelegatingFilterProxy(在 web.xml 檔案中定義)委派給此篩選器鏈。實作此委派篩選器鏈(或可能的多個鏈)的類別稱為FilterChainProxy。您可以將 <http> 區塊視為建立FilterChainProxy Bean. FilterChainProxy具有一個名為filterChainMap的屬性,它是一個從模式到篩選器 Bean 列表的映射。因此,例如,您可能會有類似這樣的內容

    <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 等是應用程式 context 中實作javax.servlet.Filter介面的其他 Bean 的名稱。

因此FilterChainProxy將傳入的請求匹配到篩選器列表,並將請求傳遞到它找到的第一個匹配鏈。請注意,除了 "/images/*" 模式(它映射到一個空的篩選器鏈)之外,這些與 <intercept-url< 命名空間元素中的模式沒有關聯。<http> 組態目前只能維護映射到所有請求的單個篩選器列表(除了那些組態為完全繞過篩選器鏈的請求)。

由於上面的組態有點冗長,因此有一種更緊湊的命名空間語法可用於組態FilterChainProxy映射,而不會丟失任何功能。上面的等效項將是


    <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(有關範例,請參閱此常見問題解答)。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預設情況下),清除安全性 context 並使會話失效。AnonymousAuthenticationFilter負責為匿名使用者填充安全性 context,使其更容易應用預設安全性限制,這些限制對於某些 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。它在您的控制器 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 社群中所有即將舉辦的活動。

檢視全部