API 文档
您可以在线浏览完整的Javadoc。关键 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 ...
}
我们创建一个
SessionRepository实例,其泛型S继承自Session。泛型类型在我们的类中定义。我们使用
SessionRepository创建一个新的Session,并将其赋值给类型为S的变量。我们与
Session进行交互。在示例中,我们演示了将User保存到Session中。现在我们保存
Session。这就是为什么我们需要泛型S。SessionRepository只允许保存通过相同SessionRepository创建或检索的Session实例。这使得SessionRepository能够进行特定于实现的优化(例如,仅写入已更改的属性)。我们从
SessionRepository中检索Session。我们从
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 ...
}
我们创建一个
SessionRepository实例,其泛型类型S继承自Session。泛型类型在我们的类中定义。我们使用
SessionRepository创建一个新的Session,并将其赋值给类型为S的变量。我们与
Session进行交互。在示例中,我们展示了更新Session在过期前可以处于非活动状态的时间。现在我们保存
Session。这就是为什么我们需要泛型类型S。SessionRepository只允许保存使用相同SessionRepository创建或检索的Session实例。这使得SessionRepository可以进行特定于实现的优化(例如,仅写入已更改的属性)。保存Session时会自动更新最后访问时间。我们从
SessionRepository中检索Session。如果Session已过期,结果将为空。
使用 SessionRepository
SessionRepository 负责创建、检索和持久化 Session 实例。
如果可能,你不应该直接与 SessionRepository 或 Session 进行交互。相反,开发者应该优先通过 HttpSession 和 WebSocket 集成间接地与 SessionRepository 和 Session 进行交互。
使用 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 实例。
如果可能,你不应该直接与 ReactiveSessionRepository 或 Session 交互。相反,你应该优先通过 WebSession 集成间接地与 ReactiveSessionRepository 和 Session 交互。
使用 @EnableSpringHttpSession
您可以在 @Configuration 类中添加 @EnableSpringHttpSession 注解,以将 SessionRepositoryFilter 作为名为 springSessionRepositoryFilter 的 bean 暴露出来。为了使用该注解,您必须提供一个 SessionRepository bean。以下示例展示了如何实现这一点:
@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {
@Bean
public MapSessionRepository sessionRepository() {
return new MapSessionRepository(new ConcurrentHashMap<>());
}
}
请注意,没有为你配置会话过期的基础设施。这是因为诸如会话过期之类的事情高度依赖于具体实现。这意味着,如果你需要清理过期的会话,你需要负责清理这些过期的会话。
使用 @EnableSpringWebSession
你可以在 @Configuration 类中添加 @EnableSpringWebSession 注解,以将 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>
该密钥的后缀是 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 支持 SessionDestroyedEvent 和 SessionCreatedEvent。
实例化 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 通过使用 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
后续章节将描述详细信息。
保存会话
每个会话都以 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 日零点 GMT 开始的毫秒数)。
-
会话在 1800 秒(30 分钟)后过期。
-
会话最后访问时间为 1404360000000(自 1970 年 1 月 1 日零点 GMT 开始的毫秒数)。
-
会话有两个属性。第一个是
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 的删除和过期keyspace notifications来分别触发SessionDeletedEvent和SessionExpiredEvent。SessionDeletedEvent或SessionExpiredEvent确保与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
当会话过期密钥被删除或过期时,keyspace 通知会触发对实际会话的查找,并触发一个 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 过期时才会被移除。
SessionDeletedEvent 和 SessionExpiredEvent
SessionDeletedEvent 和 SessionExpiredEvent 都是 SessionDestroyedEvent 的类型。
RedisIndexedSessionRepository 支持在 Session 被删除时触发 SessionDeletedEvent,或在 Session 过期时触发 SessionExpiredEvent。这是为了确保与 Session 相关的资源能够被正确清理。
例如,在与 WebSockets 集成时,SessionDestroyedEvent 负责关闭任何活动的 WebSocket 连接。
触发 SessionDeletedEvent 或 SessionExpiredEvent 是通过 SessionMessageListener 完成的,该监听器监听 Redis Keyspace 事件。为了使此功能正常工作,需要启用 Generic 命令和 Expired 事件的 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>
该密钥的后缀是 Spring Session 的会话标识符。
此密钥包含在
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 是一个使用 Spring Data 的 ReactiveRedisOperations 实现的 ReactiveSessionRepository。在 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>
该密钥的后缀是 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 示例是一个完整的应用程序,它演示了如何使用 Hazelcast 的 Spring Session。
要运行它,请使用以下命令:
./gradlew :samples:hazelcast:tomcatRun
Hazelcast Spring 示例 是一个完整的应用程序,演示了如何将 Spring Session 与 Hazelcast 和 Spring Security 一起使用。
它包括支持触发 SessionCreatedEvent、SessionDeletedEvent 和 SessionExpiredEvent 的示例 Hazelcast MapListener 实现。
要运行它,请使用以下命令:
./gradlew :samples:hazelcast-spring:tomcatRun
使用 ReactiveMapSessionRepository
ReactiveMapSessionRepository 允许将 Session 持久化到一个 Map 中,键是 Session ID,值是 Session。你可以使用 ConcurrentHashMap 的实现作为测试或便捷机制。或者,你也可以将其与分布式 Map 实现一起使用,但要求提供的 Map 必须是非阻塞的。
使用 JdbcIndexedSessionRepository
JdbcIndexedSessionRepository 是一个 SessionRepository 实现,它使用 Spring 的 JdbcOperations 将会话存储在关系型数据库中。在网络环境中,这通常与 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 框架参考文档。
使用 @EnableJdbcHttpSession
在 Web 环境中,创建新的 JdbcIndexedSessionRepository 的最简单方法是使用 @EnableJdbcHttpSession。你可以在 示例和指南(从这里开始) 中找到完整的示例用法。你可以使用以下属性来自定义配置:
-
tableName: 用于存储会话的数据库表的名称
-
maxInactiveIntervalInSeconds: 会话过期前的时间(以秒为单位)
自定义 LobHandler
你可以通过创建一个名为 springSessionLobHandler 的 bean 来自定义 BLOB 处理,该 bean 需要实现 LobHandler 接口。
自定义 ConversionService
你可以通过提供一个 ConversionService 实例来自定义会话的默认序列化和反序列化。在典型的 Spring 环境中,默认的 ConversionService bean(名为 conversionService)会被自动选取并用于序列化和反序列化。但是,你可以通过提供一个名为 springSessionConversionService 的 bean 来覆盖默认的 ConversionService。
存储详情
默认情况下,此实现使用 SPRING_SESSION 和 SPRING_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 操作都是以事务方式执行的。为了防止由于与现有事务的干扰而导致意外行为(例如,在已经参与只读事务的线程中运行 save 操作),事务是以 REQUIRES_NEW 传播级别执行的。
使用 HazelcastIndexedSessionRepository
HazelcastIndexedSessionRepository 是一个 SessionRepository 实现,它将会话存储在 Hazelcast 的分布式 IMap 中。在网络环境中,这通常与 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 才能使配置生效。你可以在 Samples and Guides (Start Here) 中找到完整的配置示例。
基本自定义
你可以在 @EnableHazelcastHttpSession 上使用以下属性来自定义配置:
-
maxInactiveIntervalInSeconds: 会话到期前的时间,以秒为单位。默认值为 1800 秒(30 分钟)
-
sessionMapName: 用于在 Hazelcast 中存储会话数据的分布式
Map的名称。
会话事件
使用 MapListener 响应分布式 Map 中条目的添加、淘汰和删除,会导致这些事件分别触发通过 ApplicationEventPublisher 发布 SessionCreatedEvent、SessionExpiredEvent 和 SessionDeletedEvent 事件。
存储详情
会话存储在 Hazelcast 中的分布式 IMap 中。IMap 接口方法用于 get() 和 put() 会话。此外,values() 方法支持 FindByIndexNameSessionRepository#findByIndexNameAndIndexValue 操作,以及适当的 ValueExtractor(需要在 Hazelcast 中注册)。有关此配置的更多详细信息,请参阅 Hazelcast Spring 示例。IMap 中会话的过期由 Hazelcast 对在将条目 put() 到 IMap 时设置生存时间的支持来处理。空闲时间超过生存时间的条目(会话)将自动从 IMap 中移除。
您不需要在Hazelcast配置中为IMap设置诸如max-idle-seconds或time-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;
}
我们将 cookie 的名称自定义为
JSESSIONID。我们将 cookie 的路径自定义为
/(而不是默认的上下文根)。我们将域名模式(一个正则表达式)自定义为
^.?\\.(\\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:SameSitecookie 指令的值。要禁用SameSitecookie 指令的序列化,可以将此值设置为null。默认值:Lax
您应该仅匹配有效的域名字符,因为域名会被反映在响应中。这样做可以防止恶意用户执行诸如HTTP 响应拆分之类的攻击。
自定义 SessionRepository
实现自定义 SessionRepository API 应该是一个相当直接的任务。将自定义实现与 @EnableSpringHttpSession 支持结合使用,可以让您重用现有的 Spring Session 配置设施和基础设施。然而,有几个方面需要更仔细地考虑。
在 HTTP 请求的生命周期中,HttpSession 通常会被持久化到 SessionRepository 两次。第一次持久化操作是为了确保客户端一旦获得会话 ID 就可以访问该会话,并且在会话提交后也需要写入,因为之后可能会对会话进行进一步的修改。考虑到这一点,我们通常建议 SessionRepository 实现跟踪更改以确保仅保存增量。这在高度并发的环境中尤为重要,在这种环境中,多个请求会对同一个 HttpSession 进行操作,因此会导致竞争条件,请求会相互覆盖对会话属性的更改。Spring Session 提供的所有 SessionRepository 实现都使用了上述方法来持久化会话更改,并且在实现自定义 SessionRepository 时可以用作参考。
请注意,实现自定义 ReactiveSessionRepository 时也适用同样的建议。在这种情况下,您应该使用 @EnableSpringWebSession。