API 文件

使用 Session

一個 Session 是一個簡化的鍵值對 Map

典型的用法可能如下所示

class RepositoryDemo<S extends Session> {

	private SessionRepository<S> repository; (1)

	void demo() {
		S toSave = this.repository.createSession(); (2)

		(3)
		User rwinch = new User("rwinch");
		toSave.setAttribute(ATTR_USER, rwinch);

		this.repository.save(toSave); (4)

		S session = this.repository.findById(toSave.getId()); (5)

		(6)
		User user = session.getAttribute(ATTR_USER);
		assertThat(user).isEqualTo(rwinch);
	}

	// ... setter methods ...

}
1 我們建立一個帶有泛型 S,擴充套件自 SessionSessionRepository 例項。泛型型別在我們類中定義。
2 我們使用我們的 SessionRepository 建立一個新的 Session 並將其賦值給一個型別為 S 的變數。
3 我們與 Session 互動。在我們的示例中,我們演示瞭如何將一個 User 儲存到 Session
4 現在我們儲存 Session。這就是為什麼我們需要泛型 SSessionRepository 只允許儲存使用同一個 SessionRepository 建立或檢索的 Session 例項。這允許 SessionRepository 進行特定實現的最佳化(即只寫入已更改的屬性)。
5 我們從 SessionRepository 檢索 Session
6 我們從我們的 Session 獲取持久化的 User,而無需顯式轉換我們的屬性。

Session API 還提供了與 Session 例項過期相關的屬性。

典型的用法可能如下所示

class ExpiringRepositoryDemo<S extends Session> {

	private SessionRepository<S> repository; (1)

	void demo() {
		S toSave = this.repository.createSession(); (2)
		// ...
		toSave.setMaxInactiveInterval(Duration.ofSeconds(30)); (3)

		this.repository.save(toSave); (4)

		S session = this.repository.findById(toSave.getId()); (5)
		// ...
	}

	// ... setter methods ...

}
1 我們建立一個帶有泛型 S,擴充套件自 SessionSessionRepository 例項。泛型型別在我們類中定義。
2 我們使用我們的 SessionRepository 建立一個新的 Session 並將其賦值給一個型別為 S 的變數。
3 我們與 Session 互動。在我們的示例中,我們演示瞭如何更新 Session 在過期前可以處於非活動狀態的時長。
4 現在我們儲存 Session。這就是為什麼我們需要泛型 SSessionRepository 只允許儲存使用同一個 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 是一個 SessionRepository,它使用 Spring Data 的 RedisOperations 實現。在 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 並實現了 RedisSerializer<Object> 的 bean 來自定義序列化。

在 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 是一個 SessionRepository,它使用 Spring Data 的 RedisOperations 實現。在 web 環境中,這通常與 SessionRepositoryFilter 結合使用。此實現透過 SessionMessageListener 支援 SessionDestroyedEventSessionCreatedEvent

例項化一個 RedisIndexedSessionRepository

以下清單展示瞭如何建立一個新例項的典型示例

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

// ... configure redisTemplate ...

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

有關如何建立 RedisConnectionFactory 的更多資訊,請參閱 Spring Data Redis 參考文件。

使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)

在 web 環境中,建立新的 RedisIndexedSessionRepository 最簡單的方法是使用 @EnableRedisHttpSession(enableIndexingAndEvents = true)。您可以在 示例與指南 (由此開始) 中找到完整的示例用法。您可以使用以下屬性自定義配置

  • enableIndexingAndEvents: 是否使用 RedisIndexedSessionRepository 而不是 RedisSessionRepository。預設值為 false

  • maxInactiveIntervalInSeconds: 會話過期前的時間量,以秒為單位。

  • redisNamespace: 允許為會話配置特定於應用的名稱空間。Redis 鍵和通道 ID 以 <redisNamespace>: 為字首開始。

  • flushMode: 允許指定何時將資料寫入 Redis。預設值是僅當在 SessionRepository 上呼叫 save 時。FlushMode.IMMEDIATE 值表示儘快將資料寫入 Redis。

自定義 RedisSerializer

您可以透過建立一個名為 springSessionDefaultRedisSerializer 並實現了 RedisSerializer<Object> 的 bean 來自定義序列化。

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(自 GMT 1970 年 1 月 1 日午夜以來的毫秒數)。

  • 會話在 1800 秒(30 分鐘)後過期。

  • 會話最後訪問於 1404360000000(自 GMT 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 連線關閉。

過期時間並非直接在 session key 本身上跟蹤,因為這意味著會話資料將不再可用。相反,使用了一個特殊的 session expires key。在前面的示例中, expires key 如下所示

APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800

當一個 session expires key 被刪除或過期時,鍵空間通知會觸發對實際會話的查詢,並觸發一個 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 連線。

觸發 SessionDeletedEventSessionExpiredEvent 透過 SessionMessageListener 實現,它監聽 Redis 鍵空間事件。為了使其工作,需要啟用通用命令和過期事件的 Redis 鍵空間事件。以下示例展示瞭如何實現

redis-cli config set notify-keyspace-events Egx

如果您使用 @EnableRedisHttpSession(enableIndexingAndEvents = true),則會自動管理 SessionMessageListener 並啟用必要的 Redis 鍵空間事件。然而,在安全的 Redis 環境中,config 命令被停用。這意味著 Spring Session 無法為您配置 Redis 鍵空間事件。要停用自動配置,請將 ConfigureRedisAction.NO_OP 新增為一個 bean。

例如,使用 Java 配置,您可以使用以下程式碼

@Bean
ConfigureRedisAction configureRedisAction() {
	return ConfigureRedisAction.NO_OP;
}

使用 XML 配置,您可以使用以下程式碼

<util:constant
	static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>

使用 SessionCreatedEvent

建立會話時,會發送一個事件到 Redis,其通道 ID 為 spring:session:channel:created:33fdd1b6-b496-4b33-9f7d-df96679d32fe,其中 33fdd1b6-b496-4b33-9f7d-df96679d32fe 是會話 ID。事件的主體是建立的會話。

如果註冊為 MessageListener(預設值),則 RedisIndexedSessionRepository 會將 Redis 訊息轉換為一個 SessionCreatedEvent

在 Redis 中檢視會話

安裝 redis-cli 後,您可以使用 redis-cli 檢查 Redis 中的值。例如,您可以在終端中輸入以下內容

$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" (1)
2) "spring:session:expirations:1418772300000" (2)
1 這個鍵的字尾是 Spring Session 的會話識別符號。
2 此鍵包含所有應在時間 1418772300000 刪除的會話 ID。

您還可以檢視每個會話的屬性。以下示例展示瞭如何實現

redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"

使用 ReactiveRedisSessionRepository

ReactiveRedisSessionRepository 是一個 ReactiveSessionRepository,它使用 Spring Data 的 ReactiveRedisOperations 實現。在 web 環境中,這通常與 WebSessionStore 結合使用。

例項化一個 ReactiveRedisSessionRepository

以下示例展示瞭如何建立一個新例項

// ... create and configure connectionFactory and serializationContext ...

ReactiveRedisTemplate<String, Object> redisTemplate = new ReactiveRedisTemplate<>(connectionFactory,
		serializationContext);

ReactiveSessionRepository<? extends Session> repository = new ReactiveRedisSessionRepository(redisTemplate);

有關如何建立 ReactiveRedisConnectionFactory 的更多資訊,請參閱 Spring Data Redis 參考文件。

使用 @EnableRedisWebSession

在 web 環境中,建立新的 ReactiveRedisSessionRepository 最簡單的方法是使用 @EnableRedisWebSession。您可以使用以下屬性自定義配置

  • maxInactiveIntervalInSeconds: 會話過期前的時間量,以秒為單位

  • redisNamespace: 允許為會話配置特定於應用的名稱空間。Redis 鍵和通道 ID 以 <redisNamespace>: 為字首開始。

  • flushMode: 允許指定何時將資料寫入 Redis。預設值是僅當在 ReactiveSessionRepository 上呼叫 save 時。FlushMode.IMMEDIATE 值表示儘快將資料寫入 Redis。

最佳化寫入

ReactiveRedisSessionRepository 管理的 Session 例項會跟蹤已更改的屬性並僅更新這些屬性。這意味著,如果一個屬性寫入一次後被多次讀取,我們只需寫入該屬性一次即可。

在 Redis 中檢視會話

安裝 redis-cli 後,您可以使用 redis-cli 檢查 Redis 中的值。例如,您可以在終端視窗中輸入以下命令

$ redis-cli
redis 127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021" (1)
1 這個鍵的字尾是 Spring Session 的會話識別符號。

您還可以使用 hkeys 命令檢視每個會話的屬性。以下示例展示瞭如何實現

redis 127.0.0.1:6379> hkeys spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021
1) "lastAccessedTime"
2) "creationTime"
3) "maxInactiveInterval"
4) "sessionAttr:username"
redis 127.0.0.1:6379> hget spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021 sessionAttr:username
"\xac\xed\x00\x05t\x00\x03rob"

使用 MapSessionRepository

MapSessionRepository 允許將 Session 持久化到 Map 中,其中鍵是 Session ID,值是 Session。您可以將此實現與 ConcurrentHashMap 一起用作測試或便捷機制。或者,您可以將其與分散式 Map 實現一起使用。例如,它可以與 Hazelcast 一起使用。

例項化 MapSessionRepository

以下示例展示瞭如何建立一個新例項

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

使用 Spring Session 和 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 Framework 參考文件

使用 @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 setter 方法自定義儲存庫使用的 SQL 查詢。在這種情況下,您需要手動配置 sessionRepository bean。

由於不同資料庫供應商之間的差異,特別是在儲存二進位制資料方面,請務必使用特定於您的資料庫的 SQL 指令碼。大多數主要資料庫供應商的指令碼打包在 org/springframework/session/jdbc/schema-*.sql 中,其中 * 是目標資料庫型別。

例如,對於 PostgreSQL,您可以使用以下 schema 指令碼

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 的支援源,您可以將 @EnableHazelcastHttpSession 註解新增到 @Configuration 類中。這樣做擴充套件了 @EnableSpringHttpSession 註解提供的功能,並在 Hazelcast 中為您建立了 SessionRepository。配置要生效,您必須提供一個單獨的 HazelcastInstance bean。您可以在 示例與指南 (由此開始) 中找到完整的配置示例。

基本自定義

您可以在 @EnableHazelcastHttpSession 上使用以下屬性自定義配置

  • maxInactiveIntervalInSeconds: 會話過期前的時間量,以秒為單位。預設值為 1800 秒(30 分鐘)

  • sessionMapName: 在 Hazelcast 中用於儲存會話資料的分散式 Map 的名稱。

會話事件

使用一個 MapListener 來響應分散式 Map 中條目被新增、逐出和移除事件,會導致這些事件(分別)透過 ApplicationEventPublisher 觸發釋出 SessionCreatedEventSessionExpiredEventSessionDeletedEvent 事件。

儲存詳情

會話儲存在 Hazelcast 的分散式 IMap 中。IMap 介面方法用於 get()put() 會話。此外,values() 方法支援一個 FindByIndexNameSessionRepository#findByIndexNameAndIndexValue 操作,並配合適當的 ValueExtractor(需要向 Hazelcast 註冊)。有關此配置的更多詳情,請參閱 Hazelcast Spring SampleIMap 中會話的過期由 Hazelcast 支援在條目被 put()IMap 時設定生存時間(TTL)來處理。閒置時間超過生存時間的條目(會話)會自動從 IMap 中移除。

您應該無需在 Hazelcast 配置中為 IMap 配置任何設定,例如 max-idle-secondstime-to-live-seconds

請注意,如果您使用 Hazelcast 的 MapStore 來持久化您的會話 IMap,則從 MapStore 重新載入會話時適用以下限制:

  • 重新載入會觸發 EntryAddedListener,導致 SessionCreatedEvent 重新發布

  • 重新載入使用給定 IMap 的預設 TTL,導致會話丟失其原始 TTL

使用 CookieSerializer

CookieSerializer 負責定義如何寫入會話 cookie。Spring Session 提供了使用 DefaultCookieSerializer 的預設實現。

CookieSerializer 暴露為 bean

當您使用像 @EnableRedisHttpSession 這樣的配置時,將 CookieSerializer 暴露為 Spring bean 會增強現有配置。

以下示例展示瞭如何做到這一點:

	@Bean
	public CookieSerializer cookieSerializer() {
		DefaultCookieSerializer serializer = new DefaultCookieSerializer();
		serializer.setCookieName("JSESSIONID"); (1)
		serializer.setCookiePath("/"); (2)
		serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); (3)
		return serializer;
	}
1 我們將 cookie 的名稱定製為 JSESSIONID
2 我們將 cookie 的路徑定製為 /(而不是預設的上下文根)。
3 我們將域名模式(一個正則表示式)定製為 ^.?\\.(\\w\\.[a-z]+)$。這允許跨域和應用程式共享會話。如果正則表示式不匹配,則不設定域,並使用現有域。如果正則表示式匹配,則第一個分組用作域。這意味著對 child.example.com 的請求會將域設定為 example.com。然而,對 localhost:8080/192.168.1.100:8080/ 的請求不會設定 cookie 的域,因此在開發環境中仍然有效,無需為生產環境進行任何更改。
您應僅匹配有效的域名字元,因為域名會反映在響應中。這樣做可以防止惡意使用者執行 HTTP Response Splitting 等攻擊。

定製 CookieSerializer

您可以透過在 DefaultCookieSerializer 上使用以下任何配置選項來定製會話 cookie 的寫入方式。

  • cookieName:要使用的 cookie 名稱。預設值:SESSION

  • useSecureCookie:指定是否應使用安全 cookie。預設值:建立時使用 HttpServletRequest.isSecure() 的值。

  • cookiePath:cookie 的路徑。預設值:上下文根。

  • cookieMaxAge:指定在建立會話時設定的 cookie 最大年齡。預設值:-1,表示瀏覽器關閉時應移除 cookie。

  • jvmRoute:指定要附加到會話 ID 幷包含在 cookie 中的字尾。用於識別路由到哪個 JVM 以實現會話親和性。對於某些實現(即 Redis),此選項不提供效能優勢。但是,它可以幫助追蹤特定使用者的日誌。

  • domainName:允許指定用於 cookie 的特定域名。此選項易於理解,但通常需要在開發和生產環境之間進行不同配置。請參閱 domainNamePattern 作為替代方案。

  • domainNamePattern:用於從 HttpServletRequest#getServerName() 中提取域名的不區分大小寫模式。模式應提供一個分組,用於提取 cookie 域的值。如果正則表示式不匹配,則不設定域,並使用現有域。如果正則表示式匹配,則第一個分組用作域。

  • sameSiteSameSite cookie 指令的值。要停用 SameSite cookie 指令的序列化,您可以將此值設定為 null。預設值:Lax

您應僅匹配有效的域名字元,因為域名會反映在響應中。這樣做可以防止惡意使用者執行 HTTP Response Splitting 等攻擊。

定製 SessionRepository

實現自定義的SessionRepository API 應該是一個相當簡單的任務。將自定義實現與@EnableSpringHttpSession 支援結合使用,可以重用現有的 Spring Session 配置功能和基礎設施。然而,有幾個方面值得仔細考慮。

在 HTTP 請求的生命週期中,HttpSession 通常會持久化到 SessionRepository 兩次。第一次持久化操作是為了確保會話在客戶端一旦獲取到會話 ID 就能使用,並且在會話提交後也需要寫入,因為可能會對會話進行進一步的修改。考慮到這一點,我們通常建議 SessionRepository 實現跟蹤更改以確保只儲存增量。這在高度併發的環境中尤為重要,其中多個請求操作同一個 HttpSession,因此會導致競爭條件,請求相互覆蓋對方對會話屬性的更改。Spring Session 提供的所有 SessionRepository 實現都使用所述方法來持久化會話更改,並且可以在您實現自定義 SessionRepository 時作為指導。

請注意,相同的建議也適用於實現自定義的ReactiveSessionRepository。在這種情況下,您應該使用@EnableSpringWebSession