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 社区所有即将到来的活动。

查看全部