领先一步
VMware 提供培训和认证,助你加速前进。
了解更多随着 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 时代经历过的人来说,是时候重温一下了。
首先让我们看一下 <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 配置中。
<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 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.
此过滤器必须包含在任何过滤器链中。它负责在请求之间存储认证信息(一个SecurityContext实例)。它还设置了在请求期间存储它的线程局部变量,并在请求完成时清除它。其默认策略是存储SecurityContext在 HTTP 会话中,因此使用了HttpSessionSecurityContextRepositorybean。
LogoutFilter仅负责处理注销链接(/j_spring_security_logout默认为此),清除安全上下文并使会话失效。AnonymousAuthenticationFilter负责为匿名用户填充安全上下文,从而更容易应用对某些 URL 放宽的默认安全限制。例如,在上述配置中,IS_AUTHENTICATED_ANONYMOUSLY属性意味着匿名用户可以访问登录页面(但不能访问其他内容)。查看手册中关于此内容的章节以获取更多信息。它的使用是可选的,你可以删除额外的AnonymousAuthenticationProvider如果你不使用它。
SecurityContextHolderAwareRequestFilter提供了标准的 servlet API 安全方法,它使用一个访问 SecurityContext 的请求包装器。如果你不需要这些方法,可以省略此过滤器。SessionManagementFilter负责在用户在当前请求期间进行认证时(例如,通过记住我认证)应用会话相关的策略。在其默认配置中,它会创建一个新的会话(复制现有会话的属性),目的是更改会话标识符,并提供针对会话固定攻击的防御。当使用 Spring Security 的并发会话控制时,也会用到它。在此配置中,UsernamePasswordAuthenticationFilter是唯一就位的认证机制,并且也注入了一个SessionFixationProtectionStrategy。这意味着我们可以安全地移除会话管理过滤器。
如果你一直仔细留意,你会注意到我们仍然缺少上述配置中的一个 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>
这是另一个由命名空间注册的 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 以及当然还有源代码本身中查找更多信息了。