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

查看所有