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 创建一个 SessionRepository 实例,该类型扩展了 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 实例到期相关的属性。

典型用法可能如下所示

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 创建一个 SessionRepository 实例,该类型扩展了 Session。泛型类型在我们的类中定义。
2 我们使用 SessionRepository 创建一个新的 Session 并将其分配给类型为 S 的变量。
3 我们与 Session 交互。在我们的示例中,我们演示了更新 Session 在过期之前可以处于非活动状态的时间量。
4 我们现在保存 Session。这就是我们需要泛型类型 S 的原因。SessionRepository 仅允许保存使用相同的 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 的某些实现提供了自动索引其他会话属性的挂钩。例如,许多实现会自动确保使用 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 的索引名称对当前 Spring Security 用户名进行索引。

会话建立索引后,您可以使用类似于以下代码的方式查找

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是一个通过使用Spring Data的RedisOperations实现的SessionRepository。在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的bean来自定义序列化,该bean实现了RedisSerializer<Object>

在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是一个通过使用Spring Data的RedisOperations实现的SessionRepository。在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的bean来自定义序列化,该bean实现了RedisSerializer<Object>

Redis TaskExecutor

RedisIndexedSessionRepository订阅接收来自Redis的事件,方法是使用RedisMessageListenerContainer。您可以通过创建名为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

后续部分描述详细信息。

保存会话

每个会话都以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实例会跟踪已更改的属性,并且仅更新这些属性。这意味着,如果一个属性写入一次并读取多次,我们只需要写入该属性一次。例如,假设前面部分列表中的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连接。

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

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文档中的过期事件的计时部分。

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

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

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

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

我们不会显式删除键,因为在某些情况下,可能存在一个竞争条件,该条件会错误地将一个键识别为已过期,而实际上它并没有过期。除了使用分布式锁(这会降低我们的性能)之外,没有办法确保过期映射的一致性。通过简单地访问键,我们确保只有在该键的TTL已过期时才会删除该键。

SessionDeletedEventSessionExpiredEvent

SessionDeletedEventSessionExpiredEvent都是SessionDestroyedEvent的类型。

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

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

通过监听 Redis Keyspace 事件的 SessionMessageListener 可以触发 SessionDeletedEventSessionExpiredEvent 事件。为了使此功能生效,需要启用 Redis Keyspace 事件中的通用命令和过期事件。以下示例展示了如何进行配置

redis-cli config set notify-keyspace-events Egx

如果您使用 @EnableRedisHttpSession(enableIndexingAndEvents = true),则会自动管理 SessionMessageListener 并启用必要的 Redis Keyspace 事件。但是,在安全的 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 后,您可以 使用 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 和 Hazelcast

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 框架参考文档

使用 @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 设置方法自定义存储库使用的 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 文档

使用 @EnableHazelcastHttpSession

要使用 Hazelcast 作为 SessionRepository 的后端存储,您可以在 @Configuration 类中添加 @EnableHazelcastHttpSession 注解。这样做扩展了 @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 示例IMap中会话的过期时间由 Hazelcast 对在将条目put()IMap时设置生存时间的支持来处理。空闲时间超过生存时间的条目(会话)将自动从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 响应拆分之类的攻击。

自定义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 响应拆分之类的攻击。

自定义SessionRepository

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

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

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