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
: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。