Spring Security 命名空间背后的机制

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

随着 Spring Security 2 中安全模式的引入,构建一个简单的安全应用程序变得容易多了。在旧版本中,用户必须单独声明和连接所有实现 Bean,导致大型且复杂的 Spring 应用程序上下文文件,这些文件难以理解和维护。学习曲线非常陡峭,我仍然记得,当我开始参与该项目(当时称为 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> 实例列表。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,它纯粹是为了支持AnonymousAuthenticationFilter,它出现在下面的 Web 配置中。

Web Bean

现在,我们将看看如何扩展 <http> 块。这比较复杂,因为创建的 Bean 与命名空间中使用的元素名称没有明显的映射关系。

FilterChainProxy

如你可能已经知道的那样,Spring Security 的 Web 功能是使用 Servlet 过滤器实现的。它在应用程序上下文中维护自己的过滤器链,并使用 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 等是应用程序上下文中其他 Bean 的名称,这些 Bean 实现javax.servlet.Filter接口。

所以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默认为),清除安全上下文并使会话失效。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 社区中所有即将举行的活动。

查看全部