22. 会话管理

HTTP 会话相关功能由 SessionManagementFilter 和 SessionAuthenticationStrategy 接口的组合处理,该接口由过滤器委托。典型的使用包括会话固定保护攻击预防、会话超时的检测和对已验证用户可能同时打开的会话数量的限制。

22.1 SessionManagementFilter

SessionManagementFilter 根据 SecurityContextHolder 的当前内容检查 SecurityContextRepository 的内容,以确定用户在当前请求期间是否已经过身份验证,通常是通过非交互式身份验证机制(如预身份验证)或记住我进行的 [17]。如果存储库包含安全上下文,则过滤器不执行任何操作。如果没有,并且线程本地 SecurityContext 包含(非匿名)Authentication 对象,则过滤器假定它们已经由堆栈中的前一个过滤器进行了身份验证。 然后,它将调用配置的 SessionAuthenticationStrategy。

如果用户当前未进行身份验证,则过滤器将检查是否已经请求了无效的会话 ID(例如,由于超时),并且如果设置了 InvalidSessionStrategy,则将调用已配置的。最常见的行为是重定向到一个固定 URL,并将其封装在标准实现 SimpleRedirectInvalidSessionStrategy 中。如前所述,当通过命名空间配置无效会话 URL 时也使用后者。

22.2 SessionAuthenticationStrategy

SessionAuthenticationStrategy 由 SessionManagementFilter 和 AbstractAuthenticationProcessingFilter 都使用,所以如果你使用的是自定义表单登录类,例如,你将需要将它注入到这两个类中。在这种情况下,一个典型的配置,结合命名空间和自定义 bean 可能是这样的:

<http>
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />
<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
	<beans:property name="sessionAuthenticationStrategy" ref="sas" />
	...
</beans:bean>

<beans:bean id="sas" class=
"org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />

注意,如果将 bean 存储在实现 HttpSessionBindingListener 的会话中,包括 Spring 会话范围的 bean,则使用默认 SessionFixationProtectionStrategy 可能会导致问题。有关此类的更多详细信息请参阅 Javadoc。

22.3 并发控制

Spring Security 能够防止主体对同一应用程序的并发身份验证超过指定次数。许多 ISV 利用这个特性来实施许可,而网络管理员喜欢这个特性,因为它有助于防止人们共享登录名。例如,你可以阻止用户 "Batman" 从两个不同的会话登录到 web 应用程序。你可以在他们尝试登录之前阻止他们的先前登录,或者你可以报告错误,防止第二次登录。注意,如果你正在使用第二种方法,那么没有显式注销的用户(例如刚刚关闭浏览器的用户)将无法再次登录,直到其原始会话期满。

命名空间支持并发控制,因此简单的配置请查看更早的命名空间章节。有时你需要定制一些东西。

该实现使用了 SessionAuthenticationStrategy 的专用版本,称为 ConcurrentSessionControlAuthenticationStrategy。

[Note] Note

以前由 ProviderManager 进行并发身份验证检查,该检查可以注入 ConcurrentSessionController。后者将检查用户是否试图超过允许的会话数量。然而,这种方法需要预先创建 HTTP 会话,这是不可取的。在 Spring Security 3 中,首先由 AuthenticationManager 对用户进行身份验证,一旦成功对其进行身份验证,就创建会话,并检查是否允许用户打开另一个会话。

要使用并发会话支持,你需要向 web.xml 添加以下内容:

<listener>
	<listener-class>
	org.springframework.security.web.session.HttpSessionEventPublisher
	</listener-class>
</listener>

此外,你还需要向你的 FilterChainProxy 中添加 ConcurrentSessionFilter。ConcurrentSessionFilter 需要两个构造函数参数,sessionRegistry,它通常指向 SessionRegistryImpl 的实例,以及 sessionInformationExpiredStrategy,定义会话过期时应用的策略。使用命名空间创建 FilterChainProxy 和其他缺省 bean 的配置可能是这样的:

<http>
<custom-filter position="CONCURRENT_SESSION_FILTER" ref="concurrencyFilter" />
<custom-filter position="FORM_LOGIN_FILTER" ref="myAuthFilter" />

<session-management session-authentication-strategy-ref="sas"/>
</http>

<beans:bean id="redirectSessionInformationExpiredStrategy"
class="org.springframework.security.web.session.SimpleRedirectSessionInformationExpiredStrategy">
<beans:constructor-arg name="invalidSessionUrl" value="/session-expired.htm" />
</beans:bean>

<beans:bean id="concurrencyFilter"
class="org.springframework.security.web.session.ConcurrentSessionFilter">
<beans:constructor-arg name="sessionRegistry" ref="sessionRegistry" />
<beans:constructor-arg name="sessionInformationExpiredStrategy" ref="redirectSessionInformationExpiredStrategy" />
</beans:bean>

<beans:bean id="myAuthFilter" class=
"org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter">
<beans:property name="sessionAuthenticationStrategy" ref="sas" />
<beans:property name="authenticationManager" ref="authenticationManager" />
</beans:bean>

<beans:bean id="sas" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy">
<beans:constructor-arg>
	<beans:list>
	<beans:bean class="org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy">
		<beans:constructor-arg ref="sessionRegistry"/>
		<beans:property name="maximumSessions" value="1" />
		<beans:property name="exceptionIfMaximumExceeded" value="true" />
	</beans:bean>
	<beans:bean class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy">
	</beans:bean>
	<beans:bean class="org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy">
		<beans:constructor-arg ref="sessionRegistry"/>
	</beans:bean>
	</beans:list>
</beans:constructor-arg>
</beans:bean>

<beans:bean id="sessionRegistry"
	class="org.springframework.security.core.session.SessionRegistryImpl" />

将监听器添加到 web.xml 会导致每次 HttpSession 开始或终止时将 ApplicationEvent 发布到 Spring ApplicationContext。这是至关重要的,因为它允许会话结束时通知 SessionRegistryImpl。如果没有它,用户将永远不能再次登录,一旦他们超过他们的会话许可,即使他们退出另一个会话或它超时。

22.3.1 为当前身份验证的用户及其会话查询 SessionRegistry

通过命名空间或使用普通 bean 设置并发控制具有以下有用的副作用,即为你提供可以直接在应用程序中使用的 SessionRegistry 的引用,因此,即使你不想限制用户可能有的会话数量,不管怎样,都有可能建立基础设施。可以将 maximumSession 属性设置为 -1,以允许无限的会话。如果使用命名空间,则可以使用 session-registry-alias 属性为内部创建的 SessionRegistry 设置别名,提供可以注入到自己的 bean 中的引用。

getAllPrincipals() 方法为你提供当前已验证的用户的列表。通过调用 getAllSessions(Object principal, boolean includeExpiredSessions) 方法,可以列出用户的会话,该方法返回 SessionInformation 对象的列表。还可以通过在 SessionInformation 实例上调用 expireNow() 来终止用户的会话。当用户返回到应用程序时,将阻止它们继续进行。例如,你可能会发现这些方法在管理应用程序中是有用的。请查看 Javadoc 以获取更多信息。



[17] 由于在身份验证请求期间不会调用过滤器,因此 SessionManagementFilter 不会检测到通过身份验证后执行重定向的机制(例如表单登录)进行的身份验证。会话管理功能必须在这些情况下单独处理。

23. 匿名身份验证

23.1 概述

通常认为,采用 "deny-by-default" 是一种良好的安全实践,其中明确指定允许和不允许其他操作。定义未经验证的用户可访问的是类似的情况,特别是对于 web 应用程序。许多网站要求用户必须为除了部分 URL(例如首页和登录页面)以外的任何东西进行认证。在这种情况下,为这些特定的 URL 定义访问配置属性比为每个安全资源定义访问配置属性更容易。换言之,有时最好说 ROLE_SOMETHING 是默认需要的,并且只允许这个规则的某些例外,例如对于应用程序的登录、注销和主页。你还可以完全从过滤器链中省略这些页面,从而绕过访问控制检查,但由于其他原因,这可能是不希望的,特别是如果页面对于经过身份验证的用户表现不同。

这就是匿名身份验证的含义。请注意,匿名身份验证的用户和未经身份验证的用户之间没有真正的概念差异。Spring Security 的匿名身份验证只为你提供了更方便的配置访问控制属性的方法。例如,对 servlet API 调用(如 getCallerPrincipal)的调用仍然返回 null,尽管 SecurityContextHolder 中实际上有一个匿名身份验证对象。

在其他情况下,匿名身份验证是有用的,例如当审计拦截器查询 SecurityContextHolder 以识别哪个主体负责给定操作时。如果类知道 SecurityContextHolder 总是包含一个 Authentication 对象,并且从不为 null,那么它们可以更健壮地编写类。

23.2 配置

当使用 HTTP 配置 Spring Security 3.0 时,会自动提供匿名身份验证支持,并且可以使用 <anonymous> 元素定制(或禁用)。除非使用传统的 bean 配置,否则不需要配置这里描述的 bean。

三个类一起提供匿名身份验证功能。AnonymousAuthenticationToken 是身份验证的实现,并存储应用于匿名主体的 GrantedAuthority。有一个对应的 AnonymousAuthenticationProvider,它链接到 ProviderManager 中,以便接受匿名身份验证令牌。最后,还有一个 AnonymousAuthenticationFilter,它被链接在正常的身份验证机制之后,如果没有保存现有的 Authentication,它会自动向 SecurityContextHolder 添加一个 AnonymousAuthenticationToken。过滤器和身份验证提供程序的定义如下:

<bean id="anonymousAuthFilter"
	class="org.springframework.security.web.authentication.AnonymousAuthenticationFilter">
<property name="key" value="foobar"/>
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS"/>
</bean>

<bean id="anonymousAuthenticationProvider"
	class="org.springframework.security.authentication.AnonymousAuthenticationProvider">
<property name="key" value="foobar"/>
</bean>

key 在过滤器和身份验证提供者之间共享,以便前者创建的令牌被后者接受 [18]。userAttribute 以 usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority] 的形式来表示。这是与 InMemoryDaoImpl 的 userMap 属性的等号之后相同的语法。

正如前面所解释的,匿名身份验证的好处是所有的 URI 模式都可以应用于安全性。例如:

<bean id="filterSecurityInterceptor"
	class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager"/>
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager"/>
<property name="securityMetadata">
	<security:filter-security-metadata-source>
	<security:intercept-url pattern='/index.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
	<security:intercept-url pattern='/hello.htm' access='ROLE_ANONYMOUS,ROLE_USER'/>
	<security:intercept-url pattern='/logoff.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
	<security:intercept-url pattern='/login.jsp' access='ROLE_ANONYMOUS,ROLE_USER'/>
	<security:intercept-url pattern='/**' access='ROLE_USER'/>
	</security:filter-security-metadata-source>" +
</property>
</bean>

23.3 AuthenticationTrustResolver

围绕匿名身份验证讨论的是 AuthenticationTrustResolver 接口及其对应的 AuthenticationTrustResolverImpl 实现。这个接口提供了一个 isAnonymous(Authentication) 方法,它允许感兴趣的类考虑这种特殊类型的身份验证状态。ExceptionTranslationFilter 在处理 AccessDeniedException 时使用这个接口。如果抛出 AccessDeniedException,并且身份验证是匿名类型,则过滤器将启动 AuthenticationEntryPoint,从而主体可以正确地进行身份验证,而不是抛出 403(禁止的)响应。这是必要的区分,否则主体将始终被视为 "authenticated",并且从来没有机会通过表单、基本、摘要或其他常规身份验证机制进行登录。

在上面的拦截器配置中,你经常会看到 ROLE_ANONYMOUS 属性被 IS_AUTHENTICATED_ANONYMOUSLY 替换,这在定义访问控制时实际上是相同的。这是一个使用 AuthenticatedVoter 的例子,我们将在身份验证章节中看到。它使用 AuthenticationTrustResolver 来处理这个特定的配置属性,并授予匿名用户的访问权限。AuthenticatedVoter 方法更强大,因为它允许你区分匿名、记住和完全认证的用户。如果你不需要这个功能,那么你可以继续使用 ROLE_ANONYMOUS,它将由 Spring Security 的标准 RoleVoter 处理。



[18] key 属性的使用不应被视为在这里提供任何真正的安全性。这仅仅是一种保持作用。如果共享一个 ProviderManager,其中包含一个 AnonymousAuthenticationProvider,其中身份验证客户端可以构造身份验证对象(例如使用 RMI 调用),那么恶意客户端可以提交一个 AnonymousAuthenticationToken,由它执行已经创建了自己(有选择的用户名和权限列表)。如果 key 是可猜测的或者可以被发现,那么令牌将被匿名提供者接受。对于正常使用来说这不是问题,但是如果你正在使用 RMI,那么最好使用定制的 ProviderManager,它省略了匿名提供者,而不是共享用于 HTTP 身份验证机制的提供者。