Spring Security 名稱空間背後

工程 | Luke Taylor | 2010年03月06日 | ...

隨著 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

我們首先看 元素,它(從 Spring Security 3.0 開始)必須在任何基於名稱空間的配置中宣告。在這個例子中, 部分依賴於這個元素(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>

在此階段,我們保留了 `` 元素,以說明它可以單獨用於建立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,它純粹是為了支援下面 Web 配置中出現的AnonymousAuthenticationFiter

Web Beans

現在我們來看看如何擴充套件 `` 塊。這更復雜,因為建立的 bean 不會像名稱空間中使用的元素名稱那樣明顯地對映。

FilterChainProxy

您可能已經知道,Spring Security 的 Web 功能是使用 Servlet 過濾器實現的。它在應用程式上下文中維護自己的過濾器鏈,並透過在 web.xml 檔案中定義的 Spring 的DelegatingFilterProxy例項委託給它。實現此委託過濾器鏈(或可能多個鏈)的類稱為FilterChainProxy。您可以將 `` 塊視為建立了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 等是應用程式上下文中實現javax.servlet.Filter介面的其他 Bean 的名稱。

所以FilterChainProxy將傳入請求與過濾器列表匹配,並透過找到的第一個匹配鏈傳遞請求。請注意,除了 "/images/*" 模式(它對映到一個空的過濾器鏈)之外,這些與 名稱空間元素中的模式無關。 配置目前只能維護一個對映到所有請求的過濾器列表(除了那些配置為完全繞過過濾器鏈的請求)。

由於上述配置有點冗長,因此可以使用更緊湊的名稱空間語法來配置一個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負責處理提交的登入表單(它由 `` 名稱空間元素建立)。該 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。在您的控制器 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 社群所有即將舉行的活動。

檢視所有