# 会话管理

## 检测超时

你可以配置 Spring 安全性以检测无效会话ID 的提交，并将用户重定向到适当的 URL。这是通过`session-management`元素实现的：

爪哇

```
@Override
protected void configure(HttpSecurity http) throws Exception{
    http
        .sessionManagement(session -> session
            .invalidSessionUrl("/invalidSession.htm")
        );
}
```

XML

```
<http>
...
<session-management invalid-session-url="/invalidSession.htm" />
</http>
```

请注意，如果你使用此机制来检测会话超时，那么如果用户注销并在不关闭浏览器的情况下重新登录，则可能会错误地报告错误。这是因为当你使会话无效时，会话cookie 将不会被清除，并且即使用户已经注销，它也将被重新提交。你可以在注销时显式地删除 JSessionID cookie，例如，在注销处理程序中使用以下语法：

爪哇

```
@Override
protected void configure(HttpSecurity http) throws Exception{
    http
        .logout(logout -> logout
            .deleteCookies("JSESSIONID")
        );
}
```

XML

```
<http>
<logout delete-cookies="JSESSIONID" />
</http>
```

不幸的是，这不能保证在每个 Servlet 容器中都能正常工作，因此你需要在你的环境中对其进行测试。

|   |如果你在代理背后运行你的应用程序，你还可以通过配置代理服务器来删除会话cookie。，<br/>，例如，使用 Apache HTTPD 的 mod\_headers，下面的指令将删除`JSESSIONID`cookie，方法是在对注销请求的响应中使其过期（假设应用程序部署在路径`/tutorial`下）：<br/><br/>```<br/><LocationMatch "/tutorial/logout"><br/>Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"<br/></LocationMatch><br/>```|
|---| |

## 并行控制会话

如果你希望对单个用户登录到你的应用程序的能力进行限制， Spring Security 通过以下简单的添加来支持这一点。首先，需要将以下监听器添加到配置中，以使 Spring 安全性更新到有关会话生命周期事件的信息：

爪哇

```
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
    return new HttpSessionEventPublisher();
}
```

XML

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

然后将以下行添加到你的应用程序上下文中：

爪哇

```
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
        );
}
```

XML

```
<http>
...
<session-management>
	<concurrency-control max-sessions="1" />
</session-management>
</http>
```

这将防止用户多次登录——第二次登录将导致第一次登录无效。通常情况下，你希望防止第二次登录，在这种情况下，你可以使用

爪哇

```
@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .sessionManagement(session -> session
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true)
        );
}
```

XML

```
<http>
<session-management>
	<concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
</session-management>
</http>
```

然后，第二次登录将被拒绝。通过“拒绝”，我们的意思是，如果使用基于表单的登录，用户将被发送到`authentication-failure-url`。如果第二次身份验证是通过另一种非交互式机制进行的，例如“remember-me”，则将向客户端发送一个“未授权的”（401）错误。如果要使用错误页，则可以将属性`session-authentication-error-url`添加到`session-management`元素中。

如果你正在为基于表单的登录使用定制的身份验证过滤器，那么你必须显式地配置并发会话控制支持。更多细节可以在[Session Management chapter](#session-mgmt)中找到。

## 会话固定攻击保护

[Session fixation](https://en.wikipedia.org/wiki/Session_fixation)攻击是一种潜在的风险，在这种情况下，恶意攻击者可能通过访问网站来创建会话，然后说服另一个用户使用相同的会话登录（例如，通过向他们发送包含会话标识符的链接作为参数）。 Spring 安全性通过在用户登录时创建新的会话或以其他方式更改会话ID 来自动防止这种情况。如果你不需要此保护，或者它与某些其他需求冲突，则可以使用`<session-management>`上的`session-fixation-protection`属性来控制该行为，该属性有四个选项

* `none`-什么都别做。原文会话将予以保留。

* `newSession`-创建新的“clean”会话，而不复制现有的会话数据（ Spring 安全性相关的属性仍将被复制）。

* `migrateSession`-创建一个新的会话并将所有现有的会话属性复制到新的会话。这是 Servlet 3.0 或更早版本容器中的默认设置。

* `changeSessionId`-不要新建会话。相反，使用由 Servlet 容器提供的会话固定保护（`HttpServletRequest#changeSessionId()`）。此选项仅在 Servlet 3.1（Java EE7）和更新的容器中可用。在较旧的容器中指定它将导致异常。这是 Servlet 3.1 和更新的容器中的默认设置。

会话发生固定保护时，会导致`SessionFixationProtectionEvent`在应用程序上下文中被发布。如果使用`changeSessionId`，此保护将导致任何*也是*的通知，因此，如果你的代码监听这两个事件，请小心。有关更多信息，请参见[Session Management](#session-mgmt)章。

## SessionManagementFilter

`SessionManagementFilter`检查`SecurityContextRepository`中的内容与`SecurityContextHolder`中的当前内容之间的对比，以确定在当前请求期间是否已对用户进行了身份验证，通常通过一种非交互式的身份验证机制，例如预认证或 remember-me<sup class="footnote">[<a id="_footnoteref_1" class="footnote" href="#_footnotedef_1" title="View footnote.">1</a>]</sup>。如果存储库包含安全上下文，则筛选器将不执行任何操作。如果不是，并且线程本地`SecurityContext`包含一个（非匿名的）`Authentication`对象，则筛选器假定它们已经由堆栈中的前一个筛选器进行了身份验证。然后它将调用配置的`SessionAuthenticationStrategy`。

如果用户当前未经过身份验证，则过滤器将检查是否请求了无效的会话ID（例如，由于超时），并将调用已配置的`InvalidSessionStrategy`（如果设置了）。最常见的行为是重定向到固定的 URL，这被封装在标准实现`SimpleRedirectInvalidSessionStrategy`中。后者也用于通过名称空间[如前所述](#session-mgmt)配置无效的会话URL。

## SessionAuthenticationStrategy

`SessionAuthenticationStrategy`被`SessionManagementFilter`和`AbstractAuthenticationProcessingFilter`都使用，因此，例如，如果你正在使用定制的 Form-Login 类，则需要将其注入到这两个类中。在这种情况下，结合名称空间和自定义 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" />
```

请注意，如果在实现`HttpSessionBindingListener`的会话中存储 bean（包括 Spring 会话-作用域 bean），则使用默认的`SessionFixationProtectionStrategy`可能会导致问题。有关此类的更多信息，请参见 Javadoc。

## 并发控制

Spring 安全性能够防止主体对同一应用程序的并行身份验证超过指定的次数。许多 ISV 利用这一点来执行许可，而网络管理员喜欢这个功能，因为它有助于防止人们共享登录名。例如，你可以阻止用户“Batman”从两个不同的会话登录到 Web 应用程序。你可以终止他们的上一次登录，或者在他们再次尝试登录时报告错误，从而阻止第二次登录。请注意，如果你正在使用第二种方法，则尚未显式退出的用户（例如，刚刚关闭其浏览器的用户）将无法再次登录，直到其原始会话到期为止。

名称空间支持并发控制，因此请检查前面的名称空间章节以获得最简单的配置。不过，有时你需要定制一些东西。

该实现使用`SessionAuthenticationStrategy`的专门版本，称为`ConcurrentSessionControlAuthenticationStrategy`。

|   |以前，并发身份验证检查是由`ProviderManager`进行的，它可以被注入`ConcurrentSessionController`，<br/>后者将检查用户是否试图超过允许的会话数量，<br/>但是，这种方法要求预先创建一个 HTTP会话，<br/>在 Spring Security3 中，首先由`AuthenticationManager`对用户进行身份验证，并且一旦他们被成功地进行了身份验证，则创建一个会话并进行检查是否允许他们打开另一个会话。|
|---| |

要使用并发会话支持，你需要在`web.xml`中添加以下内容：

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

此外，还需要将`ConcurrentSessionFilter`添加到`FilterChainProxy`中。`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`。如果没有它，一旦用户超出了他们的会话许可，他们将永远无法再次登录，即使他们退出了另一个会话或它超时。

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

通过名称空间或使用普通 bean 设置并发控制，其有益的副作用是为你提供了对`SessionRegistry`的引用，你可以在应用程序中直接使用该引用，因此，即使你不想限制用户可能拥有的会话数量，建立基础设施也是值得的。你可以将`maximumSession`属性设置为-1，以允许无限会话。如果使用名称空间，可以使用`session-registry-alias`属性为内部创建的`SessionRegistry`设置别名，提供一个引用，你可以将其注入到自己的 bean 中。

`getAllPrincipals()`方法为你提供了当前经过身份验证的用户的列表。你可以通过调用`getAllSessions(Object principal, boolean includeExpiredSessions)`方法来列出用户的会话，该方法返回`SessionInformation`对象的列表。你还可以通过在`SessionInformation`实例上调用`expireNow()`来使用户的会话过期。当用户返回应用程序时，他们将被阻止继续。例如，你可能会发现这些方法在管理应用程序中很有用。请查看 Javadoc 以获得更多信息。

---

[1](#_footnoteref_1)。通过在验证之后执行重定向的机制（例如 Form-Login）进行的验证不会被`SessionManagementFilter`检测到，因为在验证请求期间不会调用过滤器。会话-在这些情况下，管理功能必须单独处理。

[LDAP](passwords/ldap.html)[记住我](rememberme.html)

