api.md 38.2 KB
Newer Older
D
dallascao 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642
# API 文档

## [](#api-session)使用`Session`

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

## [](#api-sessionrepository)使用`SessionRepository`

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

如果可能,你不应该直接与`SessionRepository``Session`交互。相反,开发人员应该更喜欢与`SessionRepository``Session`通过[`HttpSession`]和[WebSocket](web-socket.html#websocket)集成进行间接交互。

## [](#api-findbyindexnamesessionrepository)使用`FindByIndexNameSessionRepository`

Spring 使用`Session`的会话最基本的 API 是`SessionRepository`。这个 API 有意地非常简单,因此你可以轻松地提供具有基本功能的附加实现。

一些`SessionRepository`实现方式也可以选择实现`FindByIndexNameSessionRepository`。例如, Spring 的 Redis、JDBC 和 Hazelcast 支持库都实现了`FindByIndexNameSessionRepository`

`FindByIndexNameSessionRepository`提供了一个方法来查找具有给定的索引名和索引值的所有会话。作为所有提供的`FindByIndexNameSessionRepository`实现所支持的通用用例,你可以使用一种方便的方法来查找特定用户的所有会话。这是通过确保将名称为`FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME`的会话属性填充为用户名来完成的。你有责任确保填充该属性,因为 Spring 会话不知道正在使用的身份验证机制。下面的清单中可以看到如何使用该方法的示例:

```
String username = "username";
this.session.setAttribute(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
```

|   |`FindByIndexNameSessionRepository`的一些实现方式提供钩子来自动索引其他会话属性。<br/>例如,许多实现方式自动地确保当前 Spring 安全用户名被索引的索引名为`FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME`。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

一旦对会话进行了索引,你就可以使用类似于以下代码的代码来查找:

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

## [](#api-reactivesessionrepository)使用`ReactiveSessionRepository`

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

如果可能,你不应该直接与`ReactiveSessionRepository``Session`交互。相反,你应该更喜欢与`ReactiveSessionRepository``Session`通过[WebSession](web-session.html#websession)集成进行间接交互。

## [](#api-enablespringhttpsession)使用`@EnableSpringHttpSession`

可以将`@EnableSpringHttpSession`注释添加到`@Configuration`类中,以将`SessionRepositoryFilter`公开为 Bean 名为`springSessionRepositoryFilter`的 Bean。为了使用注释,你必须提供一个`SessionRepository` Bean。下面的示例展示了如何做到这一点:

```
@EnableSpringHttpSession
@Configuration
public class SpringHttpSessionConfig {

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

}
```

请注意,没有为你配置用于会话结束的基础架构。这是因为诸如会话过期之类的事情是高度依赖于实现的。这意味着,如果你需要清理过期的会话,那么你将负责清理过期的会话。

## [](#api-enablespringwebsession)使用`@EnableSpringWebSession`

可以将`@EnableSpringWebSession`注释添加到`@Configuration`类中,以将`WebSessionManager`公开为 Bean 名为`webSessionManager`的 Bean。要使用注释,你必须提供一个`ReactiveSessionRepository` Bean。下面的示例展示了如何做到这一点:

```
@EnableSpringWebSession
public class SpringWebSessionConfig {

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

}
```

请注意,没有为你配置用于会话结束的基础架构。这是因为诸如会话过期之类的事情是高度依赖于实现的。这意味着,如果你需要清理过期的会话,那么你将负责清理过期的会话。

## [](#api-redisindexedsessionrepository)使用`RedisIndexedSessionRepository`

`RedisIndexedSessionRepository`是一个`SessionRepository`,它是通过使用 Spring 数据的`RedisOperations`来实现的。在 Web 环境中,这通常与`SessionRepositoryFilter`结合使用。该实现支持`SessionDestroyedEvent``SessionCreatedEvent``SessionMessageListener`

### [](#api-redisindexedsessionrepository-new)实例化 a`RedisIndexedSessionRepository`

你可以在下面的清单中看到如何创建新实例的典型示例:

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

// ... configure redisTemplate ...

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

有关如何创建`RedisConnectionFactory`的其他信息,请参见 Spring Data Redis 引用。

### [](#api-redisindexedsessionrepository-config)使用`@EnableRedisHttpSession`

在 Web 环境中,创建新的`RedisIndexedSessionRepository`的最简单方法是使用`@EnableRedisHttpSession`。你可以在[示例和指南(从这里开始)](samples.html#samples)中找到完整的示例用法。你可以使用以下属性来定制配置:

* **最大活动间隔秒**:会话过期前的时间,以秒为单位。

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

* **Flushmode**:允许指定何时将数据写入 Redis。只有在`SessionRepository`上调用`save`时,才会出现默认值。值`FlushMode.IMMEDIATE`将尽快写入 Redis。

#### [](#_custom_redisserializer)自定义`RedisSerializer`

你可以通过创建一个名为`springSessionDefaultRedisSerializer`的 Bean 来定制序列化,该 Bean 实现`RedisSerializer<Object>`

### [](#_redis_taskexecutor)Redis`TaskExecutor`

`RedisIndexedSessionRepository`通过使用`RedisMessageListenerContainer`订阅以接收来自 Redis 的事件。你可以通过创建一个名为`springSessionRedisTaskExecutor`的 Bean、一个名为`springSessionRedisSubscriptionExecutor`的 Bean,或同时创建这两个选项来定制这些事件的发送方式。你可以找到有关配置 Redis 任务执行器[here](https://docs.spring.io/spring-data-redis/docs/2.6.2/reference/html/#redis:pubsub:subscribe:containers)的更多详细信息。

### [](#api-redisindexedsessionrepository-storage)存储详细信息

以下各节概述了如何为每个操作更新 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
```

随后的章节将对细节进行描述。

#### [](#_saving_a_session)保存会话

每个会话都以`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`

#### [](#api-redisindexedsessionrepository-writes)优化写操作

`RedisIndexedSessionRepository`管理的`Session`实例跟踪已更改的属性,并仅更新这些属性。这意味着,如果一个属性被写了一次并读了很多次,那么我们只需要写一次该属性。例如,假设更新了上一节中的 lsiting 中的`attrName2`会话属性。保存后将运行以下命令:

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

#### [](#api-redisindexedsessionrepository-expiration)会话过期

根据`Session.getMaxInactiveInterval()`,使用`EXPIRE`命令将过期与每个会话关联。下面的示例显示了一个典型的`EXPIRE`命令:

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

请注意,在会话实际到期后将过期时间设置为五分钟。这是必要的,以便在会话过期时可以访问会话的值。在会话本身实际过期五分钟后设置一个过期,以确保它被清除,但仅在我们执行任何必要的处理之后。

|   |`SessionRepository.findById(String)`方法确保不返回过期的会话。<br/>这意味着在使用会话之前不需要检查过期。|
|---|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

Spring 会话依赖于从 Redis 中删除和过期的[键位通知](https://redis.io/topics/notifications)来发射一个[`SessionDeletedEvent`](#api-redinidexedsessionRepository-sessionDestroyedEvent)和一个[<gtr="179"/>](#api-redinidexedsessionRepository-sessionDestroyedEvent)。<gtr="180"/><gtr="181"/>确保清理与<gtr="182"/>相关的资源。例如,当你使用 Spring 会话的 WebSocket 支持时,Redis 过期或删除事件将触发与会话相关的任何 WebSocket 连接以关闭。

不会在会话密钥本身上直接跟踪过期,因为这将意味着会话数据将不再可用。相反,使用了一个特殊的会话过期键.在前面的示例中,Expires 键如下:

```
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 文档中的[过期事件的时间安排](https://redis.io/topics/notifications)部分。

为了避免过期事件不一定会发生的事实,我们可以确保每个密钥在预期过期时都会被访问。这意味着,如果 TTL 在密钥上过期,则 Redis 将删除该密钥,并在尝试访问该密钥时触发过期事件。

由于这个原因,每个会话的到期时间也被跟踪到最近的分钟。这使后台任务能够访问可能过期的会话,以确保以更确定的方式触发 REDIS 过期事件。下面的示例显示了这些事件:

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

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

|   |我们不会显式地删除这些密钥,因为在某些情况下,可能存在一个竞争条件,当某个密钥没有过期时,该条件会错误地将其标识为过期,<br/>不使用分布式锁(这会影响我们的性能),没有办法确保过期映射的一致性。<br/>通过简单地访问该密钥,我们确保只有当该密钥上的 TTL 过期时才会删除该密钥。|
|---|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### [](#api-redisindexedsessionrepository-sessiondestroyedevent)`SessionDeletedEvent`和`SessionExpiredEvent`

`SessionDeletedEvent``SessionExpiredEvent`都是`SessionDestroyedEvent`的类型。

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

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

发射`SessionDeletedEvent``SessionExpiredEvent`是通过`SessionMessageListener`提供的,后者监听[Redis Keyspace 事件](https://redis.io/topics/notifications)。为了实现这一点,需要启用泛型命令和过期事件的 Redis Keyspace 事件。下面的示例展示了如何做到这一点:

```
redis-cli config set notify-keyspace-events Egx
```

如果使用`@EnableRedisHttpSession`,那么管理`SessionMessageListener`并启用必要的 Redis 密钥空间事件将自动完成。但是,在一个安全的 Redis 环境中,config 命令是禁用的。这意味着 Spring 会话不能为你配置 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"/>
```

### [](#api-redisindexedsessionrepository-sessioncreatedevent)使用`SessionCreatedEvent`

创建会话时,将向 Redis 发送一个具有通道 ID`spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe`的事件,其中`33fdd1b6-b496-4b33-9f7d-df96679d32fe`是会话 ID。事件的主体是创建的会话。

如果注册为`MessageListener`(默认值),`RedisIndexedSessionRepository`然后将 Redis 消息转换为`SessionCreatedEvent`

### [](#api-redisindexedsessionrepository-cli)查看 Redis 中的会话

[安装 Redis-CLI](https://redis.io/topics/quickstart)之后,你可以检查 redis[使用 Redis-CLI](https://redis.io/commands#hash)中的值。例如,你可以在终端中输入以下内容:

```
$ 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 会话的会话标识符。|
|-----|-----------------------------------------------------------------------------------------|
|**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"
```

## [](#api-reactiveredissessionrepository)使用`ReactiveRedisSessionRepository`

`ReactiveRedisSessionRepository`是一个`ReactiveSessionRepository`,它是通过使用 Spring 数据的`ReactiveRedisOperations`来实现的。在 Web 环境中,这通常与`WebSessionStore`结合使用。

### [](#api-reactiveredissessionrepository-new)实例化`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 引用。

### [](#api-reactiveredissessionrepository-config)使用`@EnableRedisWebSession`

在 Web 环境中,创建新的`ReactiveRedisSessionRepository`的最简单方法是使用`@EnableRedisWebSession`。你可以使用以下属性来定制配置:

* **最大活动间隔秒**:会话过期前的时间量,以秒为单位

* **重新命名空间**:允许为会话配置特定于应用程序的名称空间。Redis 键和通道 ID 以`<redisNamespace>:`的 Q 前缀开始。

* **Flushmode**:允许指定何时将数据写入 Redis。只有当`save``ReactiveSessionRepository`上调用`save`时,才会出现默认值。值`FlushMode.IMMEDIATE`将尽快写入 Redis。

#### [](#api-reactiveredissessionrepository-writes)优化写操作

`ReactiveRedisSessionRepository`管理的`Session`实例跟踪已更改的属性,并仅更新这些属性。这意味着,如果一个属性被写了一次并读了很多次,那么我们只需要写一次该属性。

### [](#api-reactiveredissessionrepository-cli)查看 Redis 中的会话

[安装 Redis-CLI](https://redis.io/topics/quickstart)之后,你可以检查 redis[使用 Redis-CLI](https://redis.io/commands#hash)中的值。例如,你可以在终端窗口中输入以下命令:

```
$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" (1)
```

|**1**|此键的后缀是 Spring 会话的会话标识符。|
|-----|-----------------------------------------------------------------------|

你还可以使用`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"
```

## [](#api-mapsessionrepository)使用`MapSessionRepository`

`MapSessionRepository`允许在`Map`中持久化`Session`,键是`Session`ID,值是`Session`。你可以使用带有`ConcurrentHashMap`的实现作为测试或方便机制。或者,你可以将其与分布式`Map`实现一起使用。例如,它可以与 Hazelcast 一起使用。

### [](#api-mapsessionrepository-new)实例化`MapSessionRepository`

下面的示例展示了如何创建一个新实例:

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

### [](#api-mapsessionrepository-hazelcast)使用 Spring 会话和 hazlecast

[Hazelcast 样品](samples.html#samples)是一个完整的应用程序,它演示了如何使用 Spring 与 HazelCast 会话。

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

```
	./gradlew :samples:hazelcast:tomcatRun
```

[Hazelcast Spring Sample](samples.html#samples)是一个完整的应用程序,它演示了如何使用 Spring 会话与 Spring 安全性。

它包括支持发射`SessionCreatedEvent``SessionDeletedEvent``SessionExpiredEvent`的示例 HazelCast`MapListener`实现。

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

```
	./gradlew :samples:hazelcast-spring:tomcatRun
```

## [](#api-reactivemapsessionrepository)使用`ReactiveMapSessionRepository`

`ReactiveMapSessionRepository`允许在`Map`中持久化`Session`,键是`Session`ID,值是`Session`。你可以使用带有`ConcurrentHashMap`的实现作为测试或方便机制。或者,你可以将其用于分布式`Map`实现,并要求所提供的`Map`必须是非阻塞的。

## [](#api-jdbcindexedsessionrepository)使用`JdbcIndexedSessionRepository`

`JdbcIndexedSessionRepository`是一个`SessionRepository`实现,它使用 Spring 的`JdbcOperations`在关系数据库中存储会话。在 Web 环境中,这通常与`SessionRepositoryFilter`结合使用。请注意,此实现不支持会话事件的发布。

### [](#api-jdbcindexedsessionrepository-new)实例化`JdbcIndexedSessionRepository`

下面的示例展示了如何创建一个新实例:

```
JdbcTemplate jdbcTemplate = new JdbcTemplate();

// ... configure jdbcTemplate ...

TransactionTemplate transactionTemplate = new TransactionTemplate();

// ... configure transactionTemplate ...

SessionRepository<? extends Session> repository = new JdbcIndexedSessionRepository(jdbcTemplate,
		transactionTemplate);
```

有关如何创建和配置`JdbcTemplate``PlatformTransactionManager`的更多信息,请参见[Spring Framework Reference Documentation](https://docs.spring.io/spring/docs/5.3.16/spring-framework-reference/data-access.html)

### [](#api-jdbcindexedsessionrepository-config)使用`@EnableJdbcHttpSession`

在 Web 环境中,创建新的`JdbcIndexedSessionRepository`的最简单方法是使用`@EnableJdbcHttpSession`。你可以在[示例和指南(从这里开始)](samples.html#samples)中找到完整的使用示例,你可以使用以下属性来定制配置:

* **表格 Name**: Spring 会话用于存储会话的数据库表名称

* **最大活动间隔秒**:会话之前的时间将在几秒内到期

#### [](#_customizing_lobhandler)自定义`LobHandler`

你可以通过创建一个名为`springSessionLobHandler`的 Bean 来定制 BLOB 处理,该 Bean 实现`LobHandler`

#### [](#_customizing_conversionservice)定制`ConversionService`

你可以通过提供`ConversionService`实例来定制会话的默认序列化和反序列化。当在典型的 Spring 环境中工作时,默认的`ConversionService` Bean(名为`conversionService`)会自动拾取并用于序列化和反序列化。但是,你可以通过提供名为`springSessionConversionService`的 Bean 来覆盖默认的`ConversionService`

### [](#api-jdbcindexedsessionrepository-storage)存储详细信息

默认情况下,该实现使用`SPRING_SESSION``SPRING_SESSION_ATTRIBUTES`表来存储会话。请注意,你可以自定义表名,正如已经描述的那样。在这种情况下,用来存储属性的表是通过使用后缀为`_ATTRIBUTES`的提供的表名来命名的。如果需要进一步的定制,可以使用`set*Query`setter 方法定制存储库使用的 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;
```

### [](#_transaction_management)事务管理

`JdbcIndexedSessionRepository`中的所有 JDBC 操作都以事务方式执行。事务的传播设置为`REQUIRES_NEW`,以避免由于干扰现有事务而导致的意外行为(例如,在已经参与只读事务的线程中运行`save`操作)。

## [](#api-hazelcastindexedsessionrepository)使用`HazelcastIndexedSessionRepository`

`HazelcastIndexedSessionRepository`是一个`SessionRepository`实现,它将会话存储在 HazelCast 的分布式`IMap`中。在 Web 环境中,这通常与`SessionRepositoryFilter`结合使用。

### [](#api-hazelcastindexedsessionrepository-new)实例化`HazelcastIndexedSessionRepository`

下面的示例展示了如何创建一个新实例:

```
Config config = new Config();

// ... configure Hazelcast ...

HazelcastInstance hazelcastInstance = Hazelcast.newHazelcastInstance(config);

HazelcastIndexedSessionRepository repository = new HazelcastIndexedSessionRepository(hazelcastInstance);
```

有关如何创建和配置 HazelCast 实例的更多信息,请参见[Hazelcast 文档](https://docs.hazelcast.org/docs/3.12.12/manual/html-single/index.html#hazelcast-configuration)

### [](#api-enablehazelcasthttpsession)使用`@EnableHazelcastHttpSession`

要将[Hazelcast](https://hazelcast.org/)用作`SessionRepository`的后台数据源,可以将`@EnableHazelcastHttpSession`注释添加到`@Configuration`类中。这样做扩展了`@EnableSpringHttpSession`注释提供的功能,但在 HazelCast 中为你提供了`SessionRepository`。你必须提供一个`HazelcastInstance` Bean 才能使配置工作。你可以在[示例和指南(从这里开始)](samples.html#samples)中找到完整的配置示例。

### [](#api-enablehazelcasthttpsession-customize)基本定制

你可以在`@EnableHazelcastHttpSession`上使用以下属性来定制配置:

* **最大活动间隔秒**:会话过期前的时间,以秒为单位。默认值为 1800 秒(30 分钟)

* **SessionMapName**:在 HazelCast 中用于存储会话数据的分布式`Map`的名称。

### [](#api-enablehazelcasthttpsession-events)会话事件

使用`MapListener`来响应从分发的`Map`中添加、驱逐和删除的条目,会导致这些事件通过`SessionCreatedEvent``SessionExpiredEvent``SessionDeletedEvent`事件分别触发`ApplicationEventPublisher`事件的发布。

### [](#api-enablehazelcasthttpsession-storage)存储详细信息

会话存储在 Hazelcast 中的分布式`IMap`中。`IMap`接口方法用于`get()``put()`会话。此外,`values()`方法支持`FindByIndexNameSessionRepository#findByIndexNameAndIndexValue`操作,以及适当的`ValueExtractor`(需要向 Hazelcast 注册)。有关此配置的更多详细信息,请参见[ Hazelcast Spring Sample](samples.html#samples)。在`IMap`中的会话过期由 Hazelcast 的支持来处理,该支持将条目的生存时间设置为`put()``IMap`。空闲时间超过活动时间的条目(会话)将自动从`IMap`中删除。

你不需要在 Hazelcast 配置中为`IMap`配置任何设置,例如`max-idle-seconds``time-to-live-seconds`

请注意,如果你使用 HazelCast 的`MapStore`来持久化你的会话`IMap`,则在重新加载`MapStore`中的会话时,将应用以下限制:

* 重新加载触发器`EntryAddedListener`会导致`SessionCreatedEvent`被重新发布

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

## [](#api-cookieserializer)使用`CookieSerializer`

a`CookieSerializer`负责定义会话 cookie 的编写方式。 Spring 会话带有使用`DefaultCookieSerializer`的默认实现。

### [](#api-cookieserializer-bean)将`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]+)$`<br/>这允许跨域和应用程序共享会话。<br/>如果正则表达式不匹配,则不设置域,并使用现有的域。<br/>如果正则表达式匹配,第一个[grouping](https://docs.oracle.com/javase/tutorial/essential/regex/groups.html)被用作域。<br/>这意味着对[https://child.example.com](https://child.example.com)的请求将域设置为`example.com`<br/>但是,对[http://localhost:8080/](http://localhost:8080/)[https://192.168.1.100:8080/](https://192.168.1.100:8080/)的请求使 cookie 未设置,因此,在开发中仍然可以工作,而不需要对生产进行任何更改。|

|   |你应该只匹配有效的域字符,因为响应中反映了域名。<br/>这样做可以防止恶意用户执行[HTTP 响应拆分](https://en.wikipedia.org/wiki/HTTP_response_splitting)之类的攻击。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

### [](#api-cookieserializer-customization)定制`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 域的值。如果正则表达式不匹配,则不设置域,并使用现有的域。如果正则表达式匹配,则使用第一个[grouping](https://docs.oracle.com/javase/tutorial/essential/regex/groups.html)作为域。

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

|   |你应该只匹配有效的域字符,因为响应中反映了域名。<br/>这样做可以防止恶意用户执行[HTTP 响应拆分](https://en.wikipedia.org/wiki/HTTP_response_splitting)之类的攻击。|
|---|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|

## [](#custom-sessionrepository)自定义`SessionRepository`

实现自定义[`SessionRepository`](#API-SessionRepository)API 应该是一项相当简单的任务。将自定义实现与[`@EnableSpringHttpSession`](#api-enablespringhtpsession)支持相耦合,可以重用现有的 Spring 会话配置设施和基础设施。然而,有几个方面值得更仔细地考虑。

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

请注意,同样的建议也适用于实现自定义[`ReactiveSessionRepository`](#API-reactivesessionRepository)。在这种情况下,你应该使用[`@EnableSpringWebSession`]。