API 文档

使用 Session

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 我们创建一个具有泛型类型 SSessionRepository 实例,该类型扩展了 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 我们创建一个具有泛型类型 SSessionRepository 实例,该类型扩展了 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 的某些实现提供了挂钩来自动索引其他会话属性。例如,许多实现会自动确保当前 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 是一个使用 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 时才会写入 Redis。值为 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 时才会写入 Redis。值为 FlushMode.IMMEDIATE 会尽快写入 Redis。

自定义 RedisSerializer

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

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(自 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

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

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

SessionDeletedEventSessionExpiredEvent

SessionDeletedEventSessionExpiredEvent 都是 SessionDestroyedEvent 的类型。

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

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

通过 SessionMessageListener 提供触发 SessionDeletedEventSessionExpiredEvent 的功能,该监听器监听 Redis Keyspace 事件。为了使此功能正常工作,需要启用通用命令和过期事件的 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>: 的前缀 q 开头。

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

使用 @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 中、被逐出或从分布式 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 公开

CookieSerializer 作为 Spring Bean 公开,会在您使用 @EnableRedisHttpSession 等配置时增强现有配置。

以下示例展示了如何做到这一点。

	@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 域值的单一分组。如果正则表达式不匹配,则不设置任何域,并使用现有域。如果正则表达式匹配,则第一个 分组 将用作域。

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

您应该只匹配有效的域名字符,因为域名会反映在响应中。这样做可以防止恶意用户执行诸如 HTTP 响应拆分 之类的攻击。

自定义 SessionRepository

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

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

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