API 文档

使用 Session

一个 Session 是一个简化的键值对 Map

典型的用法可能如下所示

class RepositoryDemo<S extends Session> {

	private SessionRepository<S> repository; (1)

	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 我们创建一个带有泛型 S,扩展自 SessionSessionRepository 实例。泛型类型在我们类中定义。
2 我们使用我们的 SessionRepository 创建一个新的 Session 并将其赋值给一个类型为 S 的变量。
3 我们与 Session 交互。在我们的示例中,我们演示了如何将一个 User 保存到 Session
4 现在我们保存 Session。这就是为什么我们需要泛型 SSessionRepository 只允许保存使用同一个 SessionRepository 创建或检索的 Session 实例。这允许 SessionRepository 进行特定实现的优化(即只写入已更改的属性)。
5 我们从 SessionRepository 检索 Session
6 我们从我们的 Session 获取持久化的 User,而无需显式转换我们的属性。

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

典型的用法可能如下所示

class ExpiringRepositoryDemo<S extends Session> {

	private SessionRepository<S> repository; (1)

	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 我们创建一个带有泛型 S,扩展自 SessionSessionRepository 实例。泛型类型在我们类中定义。
2 我们使用我们的 SessionRepository 创建一个新的 Session 并将其赋值给一个类型为 S 的变量。
3 我们与 Session 交互。在我们的示例中,我们演示了如何更新 Session 在过期前可以处于非活动状态的时长。
4 现在我们保存 Session。这就是为什么我们需要泛型 SSessionRepository 只允许保存使用同一个 SessionRepository 创建或检索的 Session 实例。这允许 SessionRepository 进行特定实现的优化(即只写入已更改的属性)。Session 保存时,最后访问时间会自动更新。
5 我们从 SessionRepository 检索 Session。如果 Session 已过期,结果将为 null。

使用 SessionRepository

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

如果可能,您不应直接与 SessionRepositorySession 交互。相反,开发者应优先通过 HttpSessionWebSocket 集成间接与 SessionRepositorySession 交互。

使用 FindByIndexNameSessionRepository

Spring Session 使用 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 的一些实现提供了自动索引其他会话属性的钩子。例如,许多实现会自动确保当前的 Spring Security 用户名使用索引名称 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 进行索引。

一旦会话被索引,您可以使用类似于以下代码的方式进行查找

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

使用 ReactiveSessionRepository

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

如果可能,您不应直接与 ReactiveSessionRepositorySession 交互。相反,您应优先通过 WebSession 集成间接与 ReactiveSessionRepositorySession 交互。

使用 @EnableSpringHttpSession

您可以将 @EnableSpringHttpSession 注解添加到 @Configuration 类中,以将 SessionRepositoryFilter 公开为一个名为 springSessionRepositoryFilter 的 bean。为了使用该注解,您必须提供一个单独的 SessionRepository bean。以下示例展示了如何实现

@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {

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

}

请注意,未为您配置会话过期的基础设施。这是因为会话过期等事项高度依赖于具体的实现。这意味着,如果您需要清理已过期的会话,您有责任进行清理。

使用 @EnableSpringWebSession

您可以将 @EnableSpringWebSession 注解添加到 @Configuration 类中,以将 WebSessionManager 公开为一个名为 webSessionManager 的 bean。要使用该注解,您必须提供一个单独的 ReactiveSessionRepository bean。以下示例展示了如何实现

@Configuration(proxyBeanMethods = false)
@EnableSpringWebSession
public class SpringWebSessionConfig {

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

}

请注意,未为您配置会话过期的基础设施。这是因为会话过期等事项高度依赖于具体的实现。这意味着,如果您需要清理已过期的会话,您有责任进行清理。

使用 RedisSessionRepository

RedisSessionRepository 是一个 SessionRepository,它使用 Spring Data 的 RedisOperations 实现。在 web 环境中,这通常与 SessionRepositoryFilter 结合使用。请注意,此实现不支持发布会话事件。

实例化一个 RedisSessionRepository

以下清单展示了如何创建一个新实例的典型示例

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

// ... configure redisTemplate ...

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

有关如何创建 RedisConnectionFactory 的更多信息,请参阅 Spring Data Redis 参考文档。

使用 @EnableRedisHttpSession

在 web 环境中,创建新的 RedisSessionRepository 最简单的方法是使用 @EnableRedisHttpSession。您可以在 示例与指南 (由此开始) 中找到完整的示例用法。您可以使用以下属性自定义配置

enableIndexingAndEvents * enableIndexingAndEvents: 是否使用 RedisIndexedSessionRepository 而不是 RedisSessionRepository。默认值为 false。 * maxInactiveIntervalInSeconds: 会话过期前的时间量,以秒为单位。 * redisNamespace: 允许为会话配置特定于应用的命名空间。Redis 键和通道 ID 以 <redisNamespace>: 为前缀开始。 * flushMode: 允许指定何时将数据写入 Redis。默认值是仅当在 SessionRepository 上调用 save 时。FlushMode.IMMEDIATE 值表示尽快将数据写入 Redis。

自定义 RedisSerializer

您可以通过创建一个名为 springSessionDefaultRedisSerializer 并实现了 RedisSerializer<Object> 的 bean 来自定义序列化。

在 Redis 中查看会话

安装 redis-cli 后,您可以使用 redis-cli 检查 Redis 中的值。例如,您可以在终端窗口中输入以下命令

$ 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"

使用 RedisIndexedSessionRepository

RedisIndexedSessionRepository 是一个 SessionRepository,它使用 Spring Data 的 RedisOperations 实现。在 web 环境中,这通常与 SessionRepositoryFilter 结合使用。此实现通过 SessionMessageListener 支持 SessionDestroyedEventSessionCreatedEvent

实例化一个 RedisIndexedSessionRepository

以下清单展示了如何创建一个新实例的典型示例

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

// ... configure redisTemplate ...

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

有关如何创建 RedisConnectionFactory 的更多信息,请参阅 Spring Data Redis 参考文档。

使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)

在 web 环境中,创建新的 RedisIndexedSessionRepository 最简单的方法是使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)。您可以在 示例与指南 (由此开始) 中找到完整的示例用法。您可以使用以下属性自定义配置

  • enableIndexingAndEvents: 是否使用 RedisIndexedSessionRepository 而不是 RedisSessionRepository。默认值为 false

  • maxInactiveIntervalInSeconds: 会话过期前的时间量,以秒为单位。

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

  • flushMode: 允许指定何时将数据写入 Redis。默认值是仅当在 SessionRepository 上调用 save 时。FlushMode.IMMEDIATE 值表示尽快将数据写入 Redis。

自定义 RedisSerializer

您可以通过创建一个名为 springSessionDefaultRedisSerializer 并实现了 RedisSerializer<Object> 的 bean 来自定义序列化。

Redis TaskExecutor

RedisIndexedSessionRepository 通过使用 RedisMessageListenerContainer 订阅接收来自 Redis 的事件。您可以通过创建一个名为 springSessionRedisTaskExecutor 的 bean、一个名为 springSessionRedisSubscriptionExecutor 的 bean,或两者都创建来定制这些事件的分发方式。您可以在这里找到关于配置 Redis 任务执行器的更多详细信息。

存储详情

以下章节概述了 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

后续章节描述了具体细节。

保存会话

每个会话在 Redis 中以一个 Hash 的形式存储。每个会话通过使用 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(自 GMT 1970 年 1 月 1 日午夜以来的毫秒数)。

  • 会话在 1800 秒(30 分钟)后过期。

  • 会话最后访问于 1404360000000(自 GMT 1970 年 1 月 1 日午夜以来的毫秒数)。

  • 该会话有两个属性。第一个是 attrName,其值为 someAttrValue。第二个会话属性名为 attrName2,其值为 someAttrValue2

优化写入

RedisIndexedSessionRepository 管理的 Session 实例会跟踪已更改的属性并仅更新这些属性。这意味着,如果一个属性写入一次后被多次读取,我们只需写入该属性一次即可。例如,假设前面清单中的 attrName2 会话属性已更新。保存时将运行以下命令

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

会话过期

通过使用 EXPIRE 命令,并基于 Session.getMaxInactiveInterval(),每个会话都会关联一个过期时间。以下示例展示了一个典型的 EXPIRE 命令

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

请注意,设置的过期时间是在会话实际过期后再延长五分钟。这是必要的,以便在会话过期时仍能访问会话的值。在会话实际过期后再延长五分钟在会话本身上设置过期,以确保它会被清理,但只有在我们执行任何必要的处理之后才进行。

SessionRepository.findById(String) 方法确保不会返回已过期的会话。这意味着您在使用会话之前无需检查是否过期。

Spring Session 依靠来自 Redis 的删除和过期键空间通知来分别触发 SessionDeletedEventSessionExpiredEventSessionDeletedEventSessionExpiredEvent 确保与 Session 相关的资源被清理。例如,当您使用 Spring Session 的 WebSocket 支持时,Redis 的过期或删除事件会触发与该会话相关的任何 WebSocket 连接关闭。

过期时间并非直接在 session key 本身上跟踪,因为这意味着会话数据将不再可用。相反,使用了一个特殊的 session expires key。在前面的示例中, expires key 如下所示

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

当一个 session expires key 被删除或过期时,键空间通知会触发对实际会话的查找,并触发一个 SessionDestroyedEvent 事件。

完全依赖 Redis 过期的一个问题是,如果键未被访问过,Redis 不保证何时触发过期事件。具体来说,Redis 用于清理过期键的后台任务是一个低优先级任务,可能不会触发键过期。更多详细信息,请参阅 Redis 文档中的过期事件的时机章节。

为了规避过期事件不保证发生的这一事实,我们可以确保在预期过期时访问每个键。这意味着,如果键的 TTL 已过期,当我们尝试访问该键时,Redis 会移除该键并触发过期事件。

因此,每个会话的过期时间也会跟踪到最近的一分钟。这允许后台任务访问可能已过期的会话,以确保 Redis 过期事件以更确定的方式触发。以下示例展示了这些事件

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

然后,后台任务使用这些映射显式请求每个键。通过访问而不是删除键,我们确保 Redis 只有在 TTL 已过期时才为我们删除该键。

我们不显式删除键,因为在某些情况下,可能存在竞态条件,会话并未过期却被错误地识别为已过期。除非使用分布式锁(这会严重影响性能),否则无法确保过期映射的一致性。通过简单地访问键,我们确保只有在该键的 TTL 过期时才将其移除。

SessionDeletedEventSessionExpiredEvent

SessionDeletedEventSessionExpiredEvent 都是 SessionDestroyedEvent 的类型。

RedisIndexedSessionRepository 支持在 Session 被删除时触发 SessionDeletedEvent,或在 Session 过期时触发 SessionExpiredEvent。这对于确保与 Session 相关的资源得到正确清理是必要的。

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

触发 SessionDeletedEventSessionExpiredEvent 通过 SessionMessageListener 实现,它监听 Redis 键空间事件。为了使其工作,需要启用通用命令和过期事件的 Redis 键空间事件。以下示例展示了如何实现

redis-cli config set notify-keyspace-events Egx

如果您使用 @EnableRedisHttpSession(enableIndexingAndEvents = true),则会自动管理 SessionMessageListener 并启用必要的 Redis 键空间事件。然而,在安全的 Redis 环境中,config 命令被禁用。这意味着 Spring Session 无法为您配置 Redis 键空间事件。要禁用自动配置,请将 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 后,您可以使用 redis-cli 检查 Redis 中的值。例如,您可以在终端中输入以下内容

$ 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 Data 的 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。您可以使用以下属性自定义配置

  • maxInactiveIntervalInSeconds: 会话过期前的时间量,以秒为单位

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

  • flushMode: 允许指定何时将数据写入 Redis。默认值是仅当在 ReactiveSessionRepository 上调用 save 时。FlushMode.IMMEDIATE 值表示尽快将数据写入 Redis。

优化写入

ReactiveRedisSessionRepository 管理的 Session 实例会跟踪已更改的属性并仅更新这些属性。这意味着,如果一个属性写入一次后被多次读取,我们只需写入该属性一次即可。

在 Redis 中查看会话

安装 redis-cli 后,您可以使用 redis-cli 检查 Redis 中的值。例如,您可以在终端窗口中输入以下命令

$ 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 允许将 Session 持久化到 Map 中,其中键是 Session ID,值是 Session。您可以将此实现与 ConcurrentHashMap 一起用作测试或便捷机制。或者,您可以将其与分布式 Map 实现一起使用。例如,它可以与 Hazelcast 一起使用。

实例化 MapSessionRepository

以下示例展示了如何创建一个新实例

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

使用 Spring Session 和 Hazlecast

Hazelcast 示例 是一个完整的应用,演示了如何将 Spring Session 与 Hazelcast 一起使用。

要运行它,请使用以下命令

	./gradlew :samples:hazelcast:tomcatRun

Hazelcast Spring 示例 是一个完整的应用,演示了如何将 Spring Session 与 Hazelcast 和 Spring Security 一起使用。

它包括示例 Hazelcast MapListener 实现,支持触发 SessionCreatedEventSessionDeletedEventSessionExpiredEvent 事件。

要运行它,请使用以下命令

	./gradlew :samples:hazelcast-spring:tomcatRun

使用 ReactiveMapSessionRepository

ReactiveMapSessionRepository 允许将 Session 持久化到 Map 中,其中键是 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);

有关如何创建和配置 JdbcTemplatePlatformTransactionManager 的更多信息,请参阅Spring Framework 参考文档

使用 @EnableJdbcHttpSession

在 web 环境中,创建新的 JdbcIndexedSessionRepository 最简单的方法是使用 @EnableJdbcHttpSession。您可以在 示例与指南 (由此开始) 中找到完整的示例用法。您可以使用以下属性自定义配置

  • tableName: Spring Session 用于存储会话的数据库表名

  • maxInactiveIntervalInSeconds: 会话过期前的时间量,以秒为单位

自定义 LobHandler

您可以通过创建一个名为 springSessionLobHandler 并实现了 LobHandler 的 bean 来自定义 BLOB 处理。

自定义 ConversionService

通过提供一个 ConversionService 实例,您可以自定义会话的默认序列化和反序列化。在典型的 Spring 环境中,默认的 ConversionService bean(名为 conversionService)会自动被检测到并用于序列化和反序列化。然而,您可以通过提供一个名为 springSessionConversionService 的 bean 来覆盖默认的 ConversionService

存储详情

默认情况下,此实现使用 SPRING_SESSIONSPRING_SESSION_ATTRIBUTES 表来存储会话。请注意,您可以自定义表名,如前所述。在这种情况下,用于存储属性的表将使用提供的表名后附加 _ATTRIBUTES 来命名。如果需要进一步自定义,您可以使用 set*Query setter 方法自定义存储库使用的 SQL 查询。在这种情况下,您需要手动配置 sessionRepository bean。

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

例如,对于 PostgreSQL,您可以使用以下 schema 脚本

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 文档

使用 @EnableHazelcastHttpSession

要使用 Hazelcast 作为 SessionRepository 的支持源,您可以将 @EnableHazelcastHttpSession 注解添加到 @Configuration 类中。这样做扩展了 @EnableSpringHttpSession 注解提供的功能,并在 Hazelcast 中为您创建了 SessionRepository。配置要生效,您必须提供一个单独的 HazelcastInstance bean。您可以在 示例与指南 (由此开始) 中找到完整的配置示例。

基本自定义

您可以在 @EnableHazelcastHttpSession 上使用以下属性自定义配置

  • maxInactiveIntervalInSeconds: 会话过期前的时间量,以秒为单位。默认值为 1800 秒(30 分钟)

  • sessionMapName: 在 Hazelcast 中用于存储会话数据的分布式 Map 的名称。

会话事件

使用一个 MapListener 来响应分布式 Map 中条目被添加、逐出和移除事件,会导致这些事件(分别)通过 ApplicationEventPublisher 触发发布 SessionCreatedEventSessionExpiredEventSessionDeletedEvent 事件。

存储详情

会话存储在 Hazelcast 的分布式 IMap 中。IMap 接口方法用于 get()put() 会话。此外,values() 方法支持一个 FindByIndexNameSessionRepository#findByIndexNameAndIndexValue 操作,并配合适当的 ValueExtractor(需要向 Hazelcast 注册)。有关此配置的更多详情,请参阅 Hazelcast Spring SampleIMap 中会话的过期由 Hazelcast 支持在条目被 put()IMap 时设置生存时间(TTL)来处理。闲置时间超过生存时间的条目(会话)会自动从 IMap 中移除。

您应该无需在 Hazelcast 配置中为 IMap 配置任何设置,例如 max-idle-secondstime-to-live-seconds

请注意,如果您使用 Hazelcast 的 MapStore 来持久化您的会话 IMap,则从 MapStore 重新加载会话时适用以下限制:

  • 重新加载会触发 EntryAddedListener,导致 SessionCreatedEvent 重新发布

  • 重新加载使用给定 IMap 的默认 TTL,导致会话丢失其原始 TTL

使用 CookieSerializer

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]+)$。这允许跨域和应用程序共享会话。如果正则表达式不匹配,则不设置域,并使用现有域。如果正则表达式匹配,则第一个分组用作域。这意味着对 child.example.com 的请求会将域设置为 example.com。然而,对 localhost:8080/192.168.1.100:8080/ 的请求不会设置 cookie 的域,因此在开发环境中仍然有效,无需为生产环境进行任何更改。
您应仅匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户执行 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 域的值。如果正则表达式不匹配,则不设置域,并使用现有域。如果正则表达式匹配,则第一个分组用作域。

  • sameSiteSameSite cookie 指令的值。要禁用 SameSite cookie 指令的序列化,您可以将此值设置为 null。默认值:Lax

您应仅匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户执行 HTTP Response Splitting 等攻击。

定制 SessionRepository

实现自定义的SessionRepository API 应该是一个相当简单的任务。将自定义实现与@EnableSpringHttpSession 支持结合使用,可以重用现有的 Spring Session 配置功能和基础设施。然而,有几个方面值得仔细考虑。

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

请注意,相同的建议也适用于实现自定义的ReactiveSessionRepository。在这种情况下,您应该使用@EnableSpringWebSession