# API 文档

## 使用`Session`

a`Session`是名称值对的简化`Map`。

典型的用法可能如下所示：

```
public class RepositoryDemo<S extends Session> {

	private SessionRepository<S> repository; (1)

	public void demo() {
		S toSave = this.repository.createSession(); (2)

		(3)
		User rwinch = new User("rwinch");
		toSave.setAttribute(ATTR_USER, rwinch);

		this.repository.save(toSave); (4)

		S session = this.repository.findById(toSave.getId()); (5)

		(6)
		User user = session.getAttribute(ATTR_USER);
		assertThat(user).isEqualTo(rwinch);
	}

	// ... setter methods ...

}
```

|**1**|我们创建一个带有泛型类型`SessionRepository`的`S`实例，该实例扩展`Session`。泛型类型是在我们的类中定义的。|
|-----|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|我们使用`SessionRepository`创建一个新的`Session`，并将其分配给类型`S`的变量。|
|**3**|我们与`Session`交互。在我们的示例中，我们演示如何将`User`保存到`Session`。|
|**4**|我们现在保存`Session`。这就是为什么我们需要泛型类型`S`。`SessionRepository`只允许保存使用相同的`SessionRepository`创建或检索的`Session`实例。这允许`SessionRepository`进行特定于实现的优化（即只编写已更改的属性）。|
|**5**|我们从`SessionRepository`中检索`Session`。|
|**6**|我们从我们的`Session`中获得持久的`User`，而不需要显式地强制转换我们的属性。|

`Session`API 还提供与`Session`实例的过期时间相关的属性。

典型的用法可能如下所示：

```
public class ExpiringRepositoryDemo<S extends Session> {

	private SessionRepository<S> repository; (1)

	public void demo() {
		S toSave = this.repository.createSession(); (2)
		// ...
		toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

		this.repository.save(toSave); (4)

		S session = this.repository.findById(toSave.getId()); (5)
		// ...
	}

	// ... setter methods ...

}
```

|**1**|我们创建一个带有泛型类型`SessionRepository`的`S`实例，该实例扩展`Session`。泛型类型是在我们的类中定义的。|
|-----|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|**2**|我们使用`SessionRepository`创建一个新的`Session`，并将其分配给类型`S`的变量。|
|**3**|我们与`Session`进行交互。<br/>在我们的示例中，我们演示了更新`Session`在过期之前可以处于非活动状态的时间。|
|**4**|我们现在保存`Session`。<br/>这就是为什么我们需要泛型类型，`S`。<br/>`SessionRepository`只允许保存使用相同的`Session`创建或检索的实例。<br/>这允许`SessionRepository`进行特定的优化（即，只写已更改的属性）。<br/>保存`Session`时，最后访问的时间将自动更新。|
|**5**|我们从`SessionRepository`中检索`Session`。<br/>如果`Session`过期，结果将为空。|

## 使用`SessionRepository`

`SessionRepository`负责创建、检索和持久化`Session`实例。

如果可能，你不应该直接与`SessionRepository`或`Session`交互。相反，开发人员应该更喜欢与`SessionRepository`和`Session`通过[`HttpSession`]和[WebSocket](web-socket.html#websocket)集成进行间接交互。

## 使用`FindByIndexNameSessionRepository`

Spring 使用`Session`的会话最基本的 API 是`SessionRepository`。这个 API 有意地非常简单，因此你可以轻松地提供具有基本功能的附加实现。

一些`SessionRepository`实现方式也可以选择实现`FindByIndexNameSessionRepository`。例如， Spring 的 Redis、JDBC 和 Hazelcast 支持库都实现了`FindByIndexNameSessionRepository`。

`FindByIndexNameSessionRepository`提供了一个方法来查找具有给定的索引名和索引值的所有会话。作为所有提供的`FindByIndexNameSessionRepository`实现所支持的通用用例，你可以使用一种方便的方法来查找特定用户的所有会话。这是通过确保将名称为`FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME`的会话属性填充为用户名来完成的。你有责任确保填充该属性，因为 Spring Session 不知道正在使用的身份验证机制。下面的清单中可以看到如何使用该方法的示例：

```
String username = "username";
this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
```

|   |`FindByIndexNameSessionRepository`的一些实现方式提供钩子来自动索引其他会话属性。<br/>例如，许多实现方式自动地确保当前 Spring 安全用户名被索引的索引名为`FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME`。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

一旦对会话进行了索引，你就可以使用类似于以下代码的代码来查找：

```
String username = "username";
Map<String, Session> sessionIdToSession = this.sessionRepository.findByPrincipalName(username);
```

## 使用`ReactiveSessionRepository`

a`ReactiveSessionRepository`负责以非阻塞和反应的方式创建、检索和持久化`Session`实例。

如果可能，你不应该直接与`ReactiveSessionRepository`或`Session`交互。相反，你应该更喜欢与`ReactiveSessionRepository`和`Session`通过[WebSession](web-session.html#websession)集成进行间接交互。

## 使用`@EnableSpringHttpSession`

可以将`@EnableSpringHttpSession`注释添加到`@Configuration`类中，以将`SessionRepositoryFilter`公开为 Bean 名为`springSessionRepositoryFilter`的 Bean。为了使用注释，你必须提供一个`SessionRepository` Bean。下面的示例展示了如何做到这一点：

```
@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {

	@Bean
	public MapSessionRepository sessionRepository() {
		return new MapSessionRepository(new ConcurrentHashMap<>());
	}

}
```

请注意，没有为你配置用于会话结束的基础架构。这是因为诸如会话过期之类的事情是高度依赖于实现的。这意味着，如果你需要清理过期的会话，那么你将负责清理过期的会话。

## 使用`@EnableSpringWebSession`

可以将`@EnableSpringWebSession`注释添加到`@Configuration`类中，以将`WebSessionManager`公开为 Bean 名为`webSessionManager`的 Bean。要使用注释，你必须提供一个`ReactiveSessionRepository` Bean。下面的示例展示了如何做到这一点：

```
@EnableSpringWebSession
public class SpringWebSessionConfig {

	@Bean
	public ReactiveSessionRepository reactiveSessionRepository() {
		return new ReactiveMapSessionRepository(new ConcurrentHashMap<>());
	}

}
```

请注意，没有为你配置用于会话结束的基础架构。这是因为诸如会话过期之类的事情是高度依赖于实现的。这意味着，如果你需要清理过期的会话，那么你将负责清理过期的会话。

## 使用`RedisIndexedSessionRepository`

`RedisIndexedSessionRepository`是一个`SessionRepository`，它是通过使用 Spring 数据的`RedisOperations`来实现的。在 Web 环境中，这通常与`SessionRepositoryFilter`结合使用。该实现支持`SessionDestroyedEvent`和`SessionCreatedEvent`到`SessionMessageListener`。

### 实例化 a`RedisIndexedSessionRepository`

你可以在下面的清单中看到如何创建新实例的典型示例：

```
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();

// ... configure redisTemplate ...

SessionRepository<? extends Session> repository = new RedisIndexedSessionRepository(redisTemplate);
```

有关如何创建`RedisConnectionFactory`的其他信息，请参见 Spring Data Redis 引用。

### 使用`@EnableRedisHttpSession`

在 Web 环境中，创建新的`RedisIndexedSessionRepository`的最简单方法是使用`@EnableRedisHttpSession`。你可以在[示例和指南（从这里开始）](samples.html#samples)中找到完整的示例用法。你可以使用以下属性来定制配置：

* **最大活动间隔秒**:会话过期前的时间，以秒为单位。

* **重新命名空间**:允许为会话配置特定于应用程序的名称空间。Redis 键和通道 ID 以`<redisNamespace>:`的前缀开始。

* **Flushmode**:允许指定何时将数据写入 Redis。只有在`SessionRepository`上调用`save`时，才会出现默认值。值`FlushMode.IMMEDIATE`将尽快写入 Redis。

#### 自定义`RedisSerializer`

你可以通过创建一个名为`springSessionDefaultRedisSerializer`的 Bean 来定制序列化，该 Bean 实现`RedisSerializer<Object>`。

### Redis`TaskExecutor`

`RedisIndexedSessionRepository`通过使用`RedisMessageListenerContainer`订阅以接收来自 Redis 的事件。你可以通过创建一个名为`springSessionRedisTaskExecutor`的 Bean、一个名为`springSessionRedisSubscriptionExecutor`的 Bean，或同时创建这两个选项来定制这些事件的发送方式。你可以找到有关配置 Redis 任务执行器[here](https://docs.spring.io/spring-data-redis/docs/2.6.2/reference/html/#redis:pubsub:subscribe:containers)的更多详细信息。

### 存储详细信息

以下各节概述了如何为每个操作更新 Redis。下面的示例展示了创建新会话的示例：

```
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \
	maxInactiveInterval 1800 \
	lastAccessedTime 1404360000000 \
	sessionAttr:attrName someAttrValue \
	sessionAttr:attrName2 someAttrValue2
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations1439245080000 2100
```

随后的章节将对细节进行描述。

#### 保存会话

每个会话都以`Hash`的形式存储在 Redis 中。通过使用`HMSET`命令设置和更新每个会话。下面的示例展示了如何存储每个会话：

```
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 \
	maxInactiveInterval 1800 \
	lastAccessedTime 1404360000000 \
	sessionAttr:attrName someAttrValue \
	sessionAttr:attrName2 someAttrValue2
```

在前面的示例中，关于会话，以下语句是正确的：

* 会话 ID 是 33FDD1B6-B496-4B33-9F7D-DF96679D32FE。

* 会话创建于 1404360000000（从 1970 年格林尼治标准时间 1 月 1 日午夜开始，以毫秒为单位）。

* 会话将在 1800 秒（30 分钟）后结束。

* 会话的最后一次访问是在 1404360000000（从 1970 年格林尼治标准时间 1 月 1 日午夜开始，以毫秒为单位）。

* 会话有两个属性。第一个是`attrName`，其值为`someAttrValue`。第二个会话属性名为`attrName2`，其值为`someAttrValue2`。

#### 优化写操作

由`RedisIndexedSessionRepository`管理的`Session`实例跟踪已更改的属性，并仅更新这些属性。这意味着，如果一个属性被写了一次并读了很多次，那么我们只需要写一次该属性。例如，假设更新了上一节中的 lsiting 中的`attrName2`会话属性。保存后将运行以下命令：

```
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe sessionAttr:attrName2 newValue
```

#### 会话过期

根据`Session.getMaxInactiveInterval()`，使用`EXPIRE`命令将过期与每个会话关联。下面的示例显示了一个典型的`EXPIRE`命令：

```
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
```

请注意，在会话实际到期后将过期时间设置为五分钟。这是必要的，以便在会话过期时可以访问会话的值。在会话本身实际过期五分钟后设置一个过期，以确保它被清除，但仅在我们执行任何必要的处理之后。

|   |`SessionRepository.findById(String)`方法确保不返回过期的会话。<br/>这意味着在使用会话之前不需要检查过期。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Spring Session 依赖于从 Redis 中删除和过期的[键位通知](https://redis.io/topics/notifications)来发射一个[`SessionDeletedEvent`]（#api-redinidexedsessionRepository-sessionDestroyedEvent）和一个[<gtr="179"/>]（#api-redinidexedsessionRepository-sessionDestroyedEvent）。<gtr="180"/>或<gtr="181"/>确保清理与<gtr="182"/>相关的资源。例如，当你使用 Spring Session 的 WebSocket 支持时，Redis 过期或删除事件将触发与会话相关的任何 WebSocket 连接以关闭。

不会在会话密钥本身上直接跟踪过期，因为这将意味着会话数据将不再可用。相反，使用了一个特殊的会话过期键.在前面的示例中，Expires 键如下：

```
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
```

当会话过期时，键被删除或过期，键位通知将触发对实际会话的查找，并触发`SessionDestroyedEvent`。

完全依赖 Redis 过期的一个问题是，如果密钥未被访问，Redis 不能保证何时触发过期事件。具体地说，Redis 用于清理过期密钥的后台任务是一个低优先级任务，可能不会触发密钥过期。有关更多详细信息，请参见 Redis 文档中的[过期事件的时间安排](https://redis.io/topics/notifications)部分。

为了避免过期事件不一定会发生的事实，我们可以确保每个密钥在预期过期时都会被访问。这意味着，如果 TTL 在密钥上过期，则 Redis 将删除该密钥，并在尝试访问该密钥时触发过期事件。

由于这个原因，每个会话的到期时间也被跟踪到最近的分钟。这使后台任务能够访问可能过期的会话，以确保以更确定的方式触发 REDIS 过期事件。下面的示例显示了这些事件：

```
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations1439245080000 2100
```

然后，后台任务使用这些映射显式地请求每个键。通过访问密钥，而不是删除它，我们确保 Redis 仅在 TTL 过期时为我们删除密钥。

|   |我们不会显式地删除这些密钥，因为在某些情况下，可能存在一个竞争条件，当某个密钥没有过期时，该条件会错误地将其标识为过期，<br/>不使用分布式锁（这会影响我们的性能），没有办法确保过期映射的一致性。<br/>通过简单地访问该密钥，我们确保只有当该密钥上的 TTL 过期时才会删除该密钥。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### `SessionDeletedEvent`和`SessionExpiredEvent`

`SessionDeletedEvent`和`SessionExpiredEvent`都是`SessionDestroyedEvent`的类型。

`RedisIndexedSessionRepository`支持在 a`Session`被删除或`SessionExpiredEvent`当 a`Session`过期时触发`SessionDeletedEvent`。这对于确保与`Session`关联的资源被正确地清理是必要的。

例如，当与 WebSockets 集成时，`SessionDestroyedEvent`负责关闭任何活动的 WebSocket 连接。

发射`SessionDeletedEvent`或`SessionExpiredEvent`是通过`SessionMessageListener`提供的，后者监听[Redis Keyspace 事件](https://redis.io/topics/notifications)。为了实现这一点，需要启用泛型命令和过期事件的 Redis Keyspace 事件。下面的示例展示了如何做到这一点：

```
redis-cli config set notify-keyspace-events Egx
```

如果使用`@EnableRedisHttpSession`，那么管理`SessionMessageListener`并启用必要的 Redis 密钥空间事件将自动完成。但是，在一个安全的 Redis 环境中，config 命令是禁用的。这意味着 Spring Session 不能为你配置 Redis Keyspace 事件。若要禁用自动配置，请将`ConfigureRedisAction.NO_OP`添加为 Bean。

例如，对于 Java 配置，你可以使用以下方法：

```
@Bean
ConfigureRedisAction configureRedisAction() {
	return ConfigureRedisAction.NO_OP;
}
```

在 XML 配置中，你可以使用以下方法：

```
<util:constant
	static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>
```

### 使用`SessionCreatedEvent`

创建会话时，将向 Redis 发送一个具有通道 ID`spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe`的事件，其中`33fdd1b6-b496-4b33-9f7d-df96679d32fe`是会话 ID。事件的主体是创建的会话。

如果注册为`MessageListener`（默认值），`RedisIndexedSessionRepository`然后将 Redis 消息转换为`SessionCreatedEvent`。

### 查看 Redis 中的会话

在[安装 Redis-CLI](https://redis.io/topics/quickstart)之后，你可以检查 redis[使用 Redis-CLI](https://redis.io/commands#hash)中的值。例如，你可以在终端中输入以下内容：

```
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" (1)
2) "spring:session:expirations:1418772300000" (2)
```

|**1**|此键的后缀是 Spring Session 的会话标识符。|
|-----|-----------------------------------------------------------------------------------------|
|**2**|此键包含在`1418772300000`时应删除的所有会话 ID。|

你还可以查看每个会话的属性。下面的示例展示了如何做到这一点：

```
redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"
```

## 使用`ReactiveRedisSessionRepository`

`ReactiveRedisSessionRepository`是一个`ReactiveSessionRepository`，它是通过使用 Spring 数据的`ReactiveRedisOperations`来实现的。在 Web 环境中，这通常与`WebSessionStore`结合使用。

### 实例化`ReactiveRedisSessionRepository`

下面的示例展示了如何创建一个新实例：

```
// ... create and configure connectionFactory and serializationContext ...

ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
		serializationContext);

ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);
```

有关如何创建`ReactiveRedisConnectionFactory`的其他信息，请参见 Spring Data Redis 引用。

### 使用`@EnableRedisWebSession`

在 Web 环境中，创建新的`ReactiveRedisSessionRepository`的最简单方法是使用`@EnableRedisWebSession`。你可以使用以下属性来定制配置：

* **最大活动间隔秒**:会话过期前的时间量，以秒为单位

* **重新命名空间**:允许为会话配置特定于应用程序的名称空间。Redis 键和通道 ID 以`<redisNamespace>:`的 Q 前缀开始。

* **Flushmode**:允许指定何时将数据写入 Redis。只有当`save`在`ReactiveSessionRepository`上调用`save`时，才会出现默认值。值`FlushMode.IMMEDIATE`将尽快写入 Redis。

#### 优化写操作

由`ReactiveRedisSessionRepository`管理的`Session`实例跟踪已更改的属性，并仅更新这些属性。这意味着，如果一个属性被写了一次并读了很多次，那么我们只需要写一次该属性。

### 查看 Redis 中的会话

在[安装 Redis-CLI](https://redis.io/topics/quickstart)之后，你可以检查 redis[使用 Redis-CLI](https://redis.io/commands#hash)中的值。例如，你可以在终端窗口中输入以下命令：

```
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" (1)
```

|**1**|此键的后缀是 Spring Session 的会话标识符。|
|-----|-----------------------------------------------------------------------|

你还可以使用`hkeys`命令查看每个会话的属性。下面的示例展示了如何做到这一点：

```
redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"
```

## 使用`MapSessionRepository`

`MapSessionRepository`允许在`Map`中持久化`Session`，键是`Session`ID，值是`Session`。你可以使用带有`ConcurrentHashMap`的实现作为测试或方便机制。或者，你可以将其与分布式`Map`实现一起使用。例如，它可以与 Hazelcast 一起使用。

### 实例化`MapSessionRepository`

下面的示例展示了如何创建一个新实例：

```
SessionRepository<? extends Session> repository = new MapSessionRepository(new ConcurrentHashMap<>());
```

### 使用 Spring Session 和 hazlecast

[Hazelcast 样品](samples.html#samples)是一个完整的应用程序，它演示了如何使用 Spring 与 HazelCast 会话。

要运行它，请使用以下命令：

```
	./gradlew :samples:hazelcast:tomcatRun
```

[Hazelcast Spring Sample](samples.html#samples)是一个完整的应用程序，它演示了如何使用 Spring Session 与 Spring 安全性。

它包括支持发射`SessionCreatedEvent`、`SessionDeletedEvent`和`SessionExpiredEvent`的示例 HazelCast`MapListener`实现。

要运行它，请使用以下命令：

```
	./gradlew :samples:hazelcast-spring:tomcatRun
```

## 使用`ReactiveMapSessionRepository`

`ReactiveMapSessionRepository`允许在`Map`中持久化`Session`，键是`Session`ID，值是`Session`。你可以使用带有`ConcurrentHashMap`的实现作为测试或方便机制。或者，你可以将其用于分布式`Map`实现，并要求所提供的`Map`必须是非阻塞的。

## 使用`JdbcIndexedSessionRepository`

`JdbcIndexedSessionRepository`是一个`SessionRepository`实现，它使用 Spring 的`JdbcOperations`在关系数据库中存储会话。在 Web 环境中，这通常与`SessionRepositoryFilter`结合使用。请注意，此实现不支持会话事件的发布。

### 实例化`JdbcIndexedSessionRepository`

下面的示例展示了如何创建一个新实例：

```
JdbcTemplate jdbcTemplate = new JdbcTemplate();

// ... configure jdbcTemplate ...

TransactionTemplate transactionTemplate = new TransactionTemplate();

// ... configure transactionTemplate ...

SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
		transactionTemplate);
```

有关如何创建和配置`JdbcTemplate`和`PlatformTransactionManager`的更多信息，请参见[Spring Framework Reference Documentation](https://docs.spring.io/spring/docs/5.3.16/spring-framework-reference/data-access.html)。

### 使用`@EnableJdbcHttpSession`

在 Web 环境中，创建新的`JdbcIndexedSessionRepository`的最简单方法是使用`@EnableJdbcHttpSession`。你可以在[示例和指南（从这里开始）](samples.html#samples)中找到完整的使用示例，你可以使用以下属性来定制配置：

* **表格 Name**: Spring Session 用于存储会话的数据库表名称

* **最大活动间隔秒**:会话之前的时间将在几秒内到期

#### 自定义`LobHandler`

你可以通过创建一个名为`springSessionLobHandler`的 Bean 来定制 BLOB 处理，该 Bean 实现`LobHandler`。

#### 定制`ConversionService`

你可以通过提供`ConversionService`实例来定制会话的默认序列化和反序列化。当在典型的 Spring 环境中工作时，默认的`ConversionService` Bean（名为`conversionService`）会自动拾取并用于序列化和反序列化。但是，你可以通过提供名为`springSessionConversionService`的 Bean 来覆盖默认的`ConversionService`。

### 存储详细信息

默认情况下，该实现使用`SPRING_SESSION`和`SPRING_SESSION_ATTRIBUTES`表来存储会话。请注意，你可以自定义表名，正如已经描述的那样。在这种情况下，用来存储属性的表是通过使用后缀为`_ATTRIBUTES`的提供的表名来命名的。如果需要进一步的定制，可以使用`set*Query`setter 方法定制存储库使用的 SQL 查询。在这种情况下，需要手动配置`sessionRepository` Bean。

由于不同数据库供应商之间的差异，特别是在存储二进制数据时，请确保使用特定于数据库的 SQL 脚本。大多数主要数据库供应商的脚本都打包为`org/springframework/session/jdbc/schema-*.sql`，其中`*`是目标数据库类型。

例如，对于 PostgreSQL，你可以使用以下模式脚本：

```
CREATE TABLE SPRING_SESSION (
	PRIMARY_ID CHAR(36) NOT NULL,
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	EXPIRY_TIME BIGINT NOT NULL,
	PRINCIPAL_NAME VARCHAR(100),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
);

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
	SESSION_PRIMARY_ID CHAR(36) NOT NULL,
	ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
	ATTRIBUTE_BYTES BYTEA NOT NULL,
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
);
```

对于 MySQL 数据库，你可以使用以下脚本：

```
CREATE TABLE SPRING_SESSION (
	PRIMARY_ID CHAR(36) NOT NULL,
	SESSION_ID CHAR(36) NOT NULL,
	CREATION_TIME BIGINT NOT NULL,
	LAST_ACCESS_TIME BIGINT NOT NULL,
	MAX_INACTIVE_INTERVAL INT NOT NULL,
	EXPIRY_TIME BIGINT NOT NULL,
	PRINCIPAL_NAME VARCHAR(100),
	CONSTRAINT SPRING_SESSION_PK PRIMARY KEY (PRIMARY_ID)
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;

CREATE UNIQUE INDEX SPRING_SESSION_IX1 ON SPRING_SESSION (SESSION_ID);
CREATE INDEX SPRING_SESSION_IX2 ON SPRING_SESSION (EXPIRY_TIME);
CREATE INDEX SPRING_SESSION_IX3 ON SPRING_SESSION (PRINCIPAL_NAME);

CREATE TABLE SPRING_SESSION_ATTRIBUTES (
	SESSION_PRIMARY_ID CHAR(36) NOT NULL,
	ATTRIBUTE_NAME VARCHAR(200) NOT NULL,
	ATTRIBUTE_BYTES BLOB NOT NULL,
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_PK PRIMARY KEY (SESSION_PRIMARY_ID, ATTRIBUTE_NAME),
	CONSTRAINT SPRING_SESSION_ATTRIBUTES_FK FOREIGN KEY (SESSION_PRIMARY_ID) REFERENCES SPRING_SESSION(PRIMARY_ID) ON DELETE CASCADE
) ENGINE=InnoDB ROW_FORMAT=DYNAMIC;
```

### 事务管理

`JdbcIndexedSessionRepository`中的所有 JDBC 操作都以事务方式执行。事务的传播设置为`REQUIRES_NEW`，以避免由于干扰现有事务而导致的意外行为（例如，在已经参与只读事务的线程中运行`save`操作）。

## 使用`HazelcastIndexedSessionRepository`

`HazelcastIndexedSessionRepository`是一个`SessionRepository`实现，它将会话存储在 HazelCast 的分布式`IMap`中。在 Web 环境中，这通常与`SessionRepositoryFilter`结合使用。

### 实例化`HazelcastIndexedSessionRepository`

下面的示例展示了如何创建一个新实例：

```
Config config = new Config();

// ... configure Hazelcast ...

HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
```

有关如何创建和配置 HazelCast 实例的更多信息，请参见[Hazelcast 文档](https://docs.hazelcast.org/docs/3.12.12/manual/html-single/index.html#hazelcast-configuration)。

### 使用`@EnableHazelcastHttpSession`

要将[Hazelcast](https://hazelcast.org/)用作`SessionRepository`的后台数据源，可以将`@EnableHazelcastHttpSession`注释添加到`@Configuration`类中。这样做扩展了`@EnableSpringHttpSession`注释提供的功能，但在 HazelCast 中为你提供了`SessionRepository`。你必须提供一个`HazelcastInstance` Bean 才能使配置工作。你可以在[示例和指南（从这里开始）](samples.html#samples)中找到完整的配置示例。

### 基本定制

你可以在`@EnableHazelcastHttpSession`上使用以下属性来定制配置：

* **最大活动间隔秒**:会话过期前的时间，以秒为单位。默认值为 1800 秒（30 分钟）

* **SessionMapName**:在 HazelCast 中用于存储会话数据的分布式`Map`的名称。

### 会话事件

使用`MapListener`来响应从分发的`Map`中添加、驱逐和删除的条目，会导致这些事件通过`SessionCreatedEvent`、`SessionExpiredEvent`和`SessionDeletedEvent`事件分别触发`ApplicationEventPublisher`事件的发布。

### 存储详细信息

会话存储在 Hazelcast 中的分布式`IMap`中。`IMap`接口方法用于`get()`和`put()`会话。此外，`values()`方法支持`FindByIndexNameSessionRepository#findByIndexNameAndIndexValue`操作，以及适当的`ValueExtractor`（需要向 Hazelcast 注册）。有关此配置的更多详细信息，请参见[ Hazelcast Spring Sample](samples.html#samples)。在`IMap`中的会话过期由 Hazelcast 的支持来处理，该支持将条目的生存时间设置为`put()`到`IMap`。空闲时间超过活动时间的条目（会话）将自动从`IMap`中删除。

你不需要在 Hazelcast 配置中为`IMap`配置任何设置，例如`max-idle-seconds`或`time-to-live-seconds`。

请注意，如果你使用 HazelCast 的`MapStore`来持久化你的会话`IMap`，则在重新加载`MapStore`中的会话时，将应用以下限制：

* 重新加载触发器`EntryAddedListener`会导致`SessionCreatedEvent`被重新发布

* 对于给定的`IMap`，重新加载使用默认的 TTL 会导致会话丢失其原始 TTL

## 使用`CookieSerializer`

a`CookieSerializer`负责定义会话 cookie 的编写方式。 Spring Session 带有使用`DefaultCookieSerializer`的默认实现。

### 将`CookieSerializer`暴露为 Bean

在使用`@EnableRedisHttpSession`之类的配置时，将`CookieSerializer`公开为 Spring  Bean 会增强现有的配置。

下面的示例展示了如何做到这一点：

```
	@Bean
	public CookieSerializer cookieSerializer() {
		DefaultCookieSerializer serializer = new DefaultCookieSerializer();
		serializer.setCookieName("JSESSIONID"); (1)
		serializer.setCookiePath("/"); (2)
		serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); (3)
		return serializer;
	}
```

|**1**|我们将 cookie 的名称自定义为`JSESSIONID`。|
|-----| |
|**2**|我们将 cookie 的路径自定义为`/`（而不是上下文根的默认值）。|
|**3**|我们将自定义域名模式（一个正则表达式）为`^.+?\\.(\\w+\\.[a-z]+)$`。<br/>这允许跨域和应用程序共享会话。<br/>如果正则表达式不匹配，则不设置域，并使用现有的域。<br/>如果正则表达式匹配，第一个[grouping](https://docs.oracle.com/javase/tutorial/essential/regex/groups.html)被用作域。<br/>这意味着对[https://child.example.com](https://child.example.com)的请求将域设置为`example.com`。<br/>但是，对[http://localhost:8080/](http://localhost:8080/)或[https://192.168.1.100:8080/](https://192.168.1.100:8080/)的请求使 cookie 未设置，因此，在开发中仍然可以工作，而不需要对生产进行任何更改。|

|   |你应该只匹配有效的域字符，因为响应中反映了域名。<br/>这样做可以防止恶意用户执行[HTTP 响应拆分](https://en.wikipedia.org/wiki/HTTP_response_splitting)之类的攻击。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### 定制`CookieSerializer`

你可以使用`DefaultCookieSerializer`上的以下任何配置选项来自定义会话 cookie 的编写方式。

* `cookieName`:要使用的 cookie 的名称。默认值：`SESSION`。

* `useSecureCookie`:指定是否应该使用安全 cookie。默认值：在创建时使用`HttpServletRequest.isSecure()`的值。

* `cookiePath`:cookie 的路径。默认值：上下文根。

* `cookieMaxAge`:指定创建会话时要设置的 cookie 的最长时间。默认值：`-1`，这表示在关闭浏览器时应删除 cookie。

* `jvmRoute`:指定要追加到会话 ID 并包含在 cookie 中的后缀。用于确定路由到哪个 JVM 以进行会话关联。对于某些实现（即 Redis），此选项不提供性能优势。但是，它可以帮助跟踪特定用户的日志。

* `domainName`:允许指定用于 cookie 的特定域名。这个选项很容易理解，但通常需要在开发环境和生产环境之间进行不同的配置。另一种选择见`domainNamePattern`。

* `domainNamePattern`:一种不区分大小写的模式，用于从`HttpServletRequest#getServerName()`中提取域名。模式应该提供一个单独的分组，用于提取 cookie 域的值。如果正则表达式不匹配，则不设置域，并使用现有的域。如果正则表达式匹配，则使用第一个[grouping](https://docs.oracle.com/javase/tutorial/essential/regex/groups.html)作为域。

* `sameSite`:cookie 指令`SameSite`的值。要禁用`SameSite`Cookie 指令的序列化，可以将该值设置为`null`。默认值：`Lax`

|   |你应该只匹配有效的域字符，因为响应中反映了域名。<br/>这样做可以防止恶意用户执行[HTTP 响应拆分](https://en.wikipedia.org/wiki/HTTP_response_splitting)之类的攻击。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## 自定义`SessionRepository`

实现自定义[`SessionRepository`]（#API-SessionRepository）API 应该是一项相当简单的任务。将自定义实现与[`@EnableSpringHttpSession`]（#api-enablespringhtpsession）支持相耦合，可以重用现有的 Spring Session 配置设施和基础设施。然而，有几个方面值得更仔细地考虑。

在 HTTP 请求的生命周期中，`HttpSession`通常会两次持久化到`SessionRepository`。第一个持久化操作是确保一旦客户端访问了会话 ID，会话对客户端是可用的，并且还需要在会话提交后写入，因为可能会对会话进行进一步的修改。考虑到这一点，我们通常建议`SessionRepository`实现跟踪更改，以确保只保存增量。这在高度并发的环境中尤其重要，在这种环境中，多个请求在同一个`HttpSession`上运行，因此会导致竞争条件，因为请求会覆盖彼此对会话属性的更改。 Spring Session 提供的所有`SessionRepository`实现都使用所描述的方法来持久化会话更改，并且可以在实现自定义`SessionRepository`时用于指导。

请注意，同样的建议也适用于实现自定义[`ReactiveSessionRepository`]（#API-reactivesessionRepository）。在这种情况下，你应该使用[`@EnableSpringWebSession`]。