從Spring-Session源碼看Session機制的實現細節


 

Re:從零開始的Spring Session(一)
Re:從零開始的Spring Session(二)
Re:從零開始的Spring Session(三)

 

 

 

 



去年我曾經寫過幾篇和 Spring Session 相關的文章,從一個未接觸過 Spring Session 的初學者視角介紹了 Spring Session 如何上手,如果你未接觸過 Spring Session,推薦先閱讀下「從零開始學習Spring Session」系列(https://www.cnkirito.moe/categories/Spring-Session/) Spring Session 主要解決了分布式場景下 Session 的共享問題,本文將從 Spring Session 的源碼出發,來討論一些 Session 設計的細節。

Spring Session 數據結構解讀

想象一個場景,現在一道面試題呈現在你面前,讓你從零開始設計一個 Session 存儲方案,你會怎么回答?

說白了就是讓你設計一套數據結構存儲 Session,並且我相信提出這個問題時,大多數讀者腦海中會浮現出 redis,設計一個 map,使用 ttl 等等,但沒想到的細節可能會更多。先來預覽一下 Spring Session 的實際數據結構是什么樣的(使用 spring-session-redis 實現),當我們訪問一次集成了Spring Session 的 web 應用時

@RequestMapping("/helloworld")
public String hello(HttpSession session){
  session.setAttribute("name","xu");
  return "hello.html";
}

 

可以在 Redis 中看到如下的數據結構:

A) "spring:session:sessions:39feb101-87d4-42c7-ab53-ac6fe0d91925"

B) "spring:session:expirations:1523934840000"

C) "spring:session:sessions:expires:39feb101-87d4-42c7-ab53-ac6fe0d91925"

 

這三種鍵職責的分析將會貫徹全文,為了統一敘述,在此將他們進行編號,后續簡稱為 A 類型鍵,B 類型鍵,C 類型鍵。先簡單分析下他們的特點

  • 他們公用的前綴是 spring:session
  • A 類型鍵的組成是前綴 +”sessions”+sessionId,對應的值是一個 hash 數據結構。在我的 demo 中,其值如下
    {
        "lastAccessedTime": 1523933008926,/*2018/4/17 10:43:28*/
        "creationTime": 1523933008926, /*2018/4/17 10:43:28*/
        "maxInactiveInterval": 1800,
        "sessionAttr:name": "xu"
    }
    其中 creationTime(創建時間),lastAccessedTime(最后訪問時間),maxInactiveInterval(session 失效的間隔時長) 等字段是系統字段,sessionAttr:xx 可能會存在多個鍵值對,用戶存放在 session 中的數據如數存放於此。

A 類型鍵對應的默認 TTL 是 35 分鍾。

  • B 類型鍵的組成是前綴+”expirations”+時間戳,無需糾結這個時間戳的含義,先賣個關子。其對應的值是一個 set 數據結構,這個 set 數據結構中存儲着一系列的 C 類型鍵。在我的 demo 中,其值如下
    [
        "expires:39feb101-87d4-42c7-ab53-ac6fe0d91925"
    ]
    B 類型鍵對應的默認 TTL 是 30 分鍾
  • C 類型鍵的組成是前綴+”sessions:expires”+sessionId,對應一個空值,它僅僅是 sessionId 在 redis 中的一個引用,具體作用繼續賣關子。

C 類型鍵對應的默認 TTL 是 30 分鍾。

小貼士:
Spring Session中操作A類鍵的API:
org.springframework.session.SessionRepository
org.springframework.session.ExpiringSession【存放A類鍵中的值】

    @Resource
    private SessionRepository<ExpiringSession> sessionRepository;

    @Override
    public ExpiringSession getSession(String sessionId) {
        ExpiringSession session = sessionRepository.getSession(sessionId);
        if (session == null) {
            log.error("session不存在 sessionId:{} ", sessionId);
            return null;
        }
        return session;
    }

 

kirito-session 的天使輪方案

介紹完 Spring Session 的數據結構,我們先放到一邊,來看看如果我們自己設計一個 Session 方案,擬定為 kirito-session 吧,該如何設計。

kirito 的心路歷程是這樣的:“使用 redis 存 session 數據,對,session 需要有過期機制,redis 的鍵可以自動過期,肯定很方便。”

於是 kirito 設計出了 spring-session 中的 A 類型鍵,復用它的數據結構:

{
    "lastAccessedTime": 1523933008926,
    "creationTime": 1523933008926, 
    "maxInactiveInterval": 1800,
    key/value...
}

然后對 A 類型的鍵設置 ttl A 30 分鍾,這樣 30分鍾之后 session 過期,0-30 分鍾期間如果用戶持續操作,那就根據 sessionId 找到 A 類型的 key,刷新 lastAccessedTime 的值,並重新設置 ttl,這樣就完成了「續簽」的特性。

顯然 Spring Session 沒有采用如此簡練的設計,為什么呢?翻看 Spring Session 的文檔

One problem with relying on Redis expiration exclusively is that Redis makes no guarantee of when the expired event will be fired if the key has not been accessed. Specifically the background task that Redis uses to clean up expired keys is a low priority task and may not trigger the key expiration. For additional details see Timing of expired events section in the Redis documentation.

大致意思是說,redis 的鍵過期機制不“保險”,這和 redis 的設計有關,不在此拓展開,研究這個的時候翻了不少資料,得出了如下的總結:

  1. redis 在鍵實際過期之后不一定會被刪除,可能會繼續存留,但具體存留的時間我沒有做過研究,可能是 1~2 分鍾,可能會更久。
  2. 具有過期時間的 key 有兩種方式來保證過期,一是這個鍵在過期的時候被訪問了,二是后台運行一個定時任務自己刪除過期的 key。划重點:這啟發我們在 key 到期后只需要訪問一下 key 就可以確保 redis 刪除該過期鍵
  3. 如果沒有指令持續關注 key,並且 redis 中存在許多與 TTL 關聯的 key,則 key 真正被刪除的時間將會有顯著的延遲!顯著的延遲!顯著的延遲!

天使輪計划慘遭破產,看來單純依賴於 redis 的過期時間是不可靠的,秉持着力求嚴謹的態度,迎來了 A 輪改造。

A 輪改造—引入 B 類型鍵確保 session 的過期機制

redis 的官方文檔啟發我們,可以啟用一個后台定時任務,定時去刪除那些過期的鍵,配合上 redis 的自動過期,這樣可以雙重保險。第一個問題來了,我們將這些過期鍵存在哪兒呢?不找個合適的地方存起來,定時任務到哪兒去刪除這些應該過期的鍵呢?總不能掃描全庫吧!來解釋我前面賣的第一個關子,看看 B 類型鍵的特點:

1
spring:session:expirations:1523934840000

時間戳的含義

1523934840000 這明顯是個 Unix 時間戳,它的含義是存放着這一分鍾內應該過期的鍵,所以它是一個 set 數據結構。解釋下這個時間戳是怎么計算出來的org.springframework.session.data.redis.RedisSessionExpirationPolicy#roundUpToNextMinute

1
2
3
4
5
6
7
8
static long roundUpToNextMinute(long timeInMs) {
Calendar date = Calendar.getInstance();
date.setTimeInMillis(timeInMs);
date.add(Calendar.MINUTE, 1);
date.clear(Calendar.SECOND);
date.clear(Calendar.MILLISECOND);
return date.getTimeInMillis();
}

還記得 lastAccessedTime=1523933008926,maxInactiveInterval=1800 吧,lastAccessedTime 轉換成北京時間是: 2018/4/17 10:43:28,向上取整是2018/4/17 10:44:00,再次轉換為 Unix 時間戳得到 1523932980000,單位是 ms,1800 是過期時間的間隔,單位是 s,二者相加 1523932980000+1800*1000=1523934840000。這樣 B 類型鍵便作為了一個「桶」,存放着這一分鍾應當過期的 session 的 key。

后台定時任務

org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanupExpiredSessions

1
2
3
4
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}")
public void cleanupExpiredSessions() {
this.expirationPolicy.cleanExpiredSessions();
}

后台提供了定時任務去“刪除”過期的 key,來補償 redis 到期未刪除的 key。方案再描述下,方便大家理解:取得當前時間的時間戳作為 key,去 redis 中定位到 spring:session:expirations:{當前時間戳} ,這個 set 里面存放的便是所有過期的 key 了。

續簽的影響

每次 session 的續簽,需要將舊桶中的數據移除,放到新桶中。驗證這一點很容易。

在第一分鍾訪問一次 http://localhost:8080/helloworld 端點,得到的 B 類型鍵為:spring:session:expirations:1523934840000;第二分鍾再訪問一次 http://localhost:8080/helloworld 端點,A 類型鍵的 lastAccessedTime 得到更新,並且 spring:session:expirations:1523934840000 這個桶被刪除了,新增了 spring:session:expirations:1523934900000 這個桶。當眾多用戶活躍時,桶的增刪和以及 set 中數據的增刪都是很頻繁的。對了,沒提到的一點,對應 key 的 ttl 時間也會被更新。

kirito-session 方案貌似比之前嚴謹了,目前為止使用了 A 類型鍵和 B 類型鍵解決了 session 存儲和 redis 鍵到期不刪除的兩個問題,但還是存在問題的。

B 輪改造—優雅地解決 B 類型鍵的並發問題

引入 B 類型鍵看似解決了問題,卻也引入了一個新的問題:並發問題。

來看看一個場景:

假設存在一個 sessionId=1 的會話,初始時間戳為 1420656360000

1
2
spring:session:expirations:1420656360000 -> [1]
spring:session:session:1 -> <session>

接下來迎來了並發訪問,(用戶可能在瀏覽器中多次點擊):

  • 線程 1 在第 2 分鍾請求,產生了續簽,session:1 應當從 1420656360000 這個桶移動到 142065642000 這個桶
  • 線程 2 在第 3 分鍾請求,也產生了續簽,session:1 本應當從 1420656360000 這個桶移動到 142065648000 這個桶
  • 如果上兩步按照次序執行,自然不會有問題。但第 3 分鍾的請求可能已經執行完畢了,第 2 分鍾才剛開始執行。

像下面這樣:

線程 2 從第一分鍾的桶中移除 session:1,並移動到第三分鍾的桶中

1
2
3
spring:session:expirations:1420656360000 -> []
spring:session:session:1 -> <session>
spring:session:expirations:1420656480000 -> [1]

線程 1 完成相同的操作,它也是基於第一分鍾來做的,但會移動到第二分鍾的桶中

1
2
3
spring:session:expirations:1420656360000 -> []
spring:session:session:1 -> <session>
spring:session:expirations:1420656420000 -> [1]

最后 redis 中鍵的情況變成了這樣:

1
2
3
4
spring:session:expirations:1420656360000 -> []
spring:session:session:1 -> <session>
spring:session:expirations:1420656480000 -> [1]
spring:session:expirations:1420656420000 -> [1]

后台定時任務會在第 32 分鍾掃描到 spring:session:expirations:1420656420000 桶中存在的 session,這意味着,本應該在第 33 分鍾才會過期的 key,在第 32 分鍾就會被刪除!

一種簡單的方法是用戶的每次 session 續期加上分布式鎖,這顯然不能被接受。來看看 Spring Session 是怎么巧妙地應對這個並發問題的。

org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanExpiredSessions

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
public void cleanExpiredSessions() {
long now = System.currentTimeMillis();
long prevMin = roundDownMinute(now);

if (logger.isDebugEnabled()) {
logger.debug("Cleaning up sessions expiring at " + new Date(prevMin));
}

// 獲取到 B 類型鍵
String expirationKey = getExpirationKey(prevMin);
// 取出當前這一分鍾應當過期的 session
Set<Object> sessionsToExpire = this.redis.boundSetOps(expirationKey).members();
// 注意:這里刪除的是 B 類型鍵,不是刪除 session 本身!
this.redis.delete(expirationKey);
for (Object session : sessionsToExpire) {
String sessionKey = getSessionKey((String) session);
// 遍歷一下 C 類型的鍵
touch(sessionKey);
}
}

/**
* By trying to access the session we only trigger a deletion if it the TTL is
* expired. This is done to handle
* https://github.com/spring-projects/spring-session/issues/93
*
* @param key the key
*/
private void touch(String key) {
// 並不是刪除 key,而只是訪問 key
this.redis.hasKey(key);
}


這里面邏輯主要是拿到過期鍵的集合(實際上是 C 類型的 key,但這里可以理解為 sessionId,C 類型我下面會介紹),此時這個集合里面存在三種類型的 sessionId。

  1. 已經被 redis 刪除的過期鍵。萬事大吉,redis 很靠譜的及時清理了過期的鍵。
  2. 已經過期,但是還沒來得及被 redis 清除的 key。還記得前面 redis 文檔里面提到的一個技巧嗎?我們在 key 到期后只需要訪問一下 key 就可以確保 redis 刪除該過期鍵,所以 redis.hasKey(key); 該操作就是為了觸發 redis 的自己刪除。
  3. 並發問題導致的多余數據,實際上並未過期。如上所述,第 32 分鍾的桶里面存在的 session:1 實際上並不應該被刪除,使用 touch 的好處便是我只負責檢測,刪不刪交給 redis 判斷。session:1 在第 32 分鍾被 touch 了一次,並未被刪除,在第 33 分鍾時應當被 redis 刪除,但可能存在延時,這個時候 touch 一次,確保刪除。

所以,源碼里面特別強調了一下:要用 touch 去觸發 key 的刪除,而不能直接 del key。

參考 https://github.com/spring-projects/spring-session/issues/93

C 輪改造—增加 C 類型鍵完善過期通知事件

雖然引入了 B 類型鍵,並且在后台加了定時器去確保 session 的過期,但似乎…emmmmm…還是不夠完善。在此之前,kirito-session 的設計方案中,存儲 session 實際內容的 A 類型鍵和用於定時器確保刪除的桶 B 類型鍵過期時間都是 30 分鍾(key 的 TTL 是 30 分鍾),注意一個細節,spring-session 中 A 類型鍵的過期時間是 35 分鍾,比實際的 30 分鍾多了 5 分鍾,這意味着即便 session 已經過期,我們還是可以在 redis 中有 5 分鍾間隔來操作過期的 session。於此同時,spring-session 引入了 C 類型鍵來作為 session 的引用。

解釋下之前賣的第二個關子,C 類型鍵的組成為前綴+”sessions:expires”+sessionId,對應一個空值,同時也是 B 類型鍵桶中存放的 session 引用,ttl 為 30 分鍾,具體作用便是在自身過期后觸發 redis 的 keyspace notifications (http://redis.io/topics/notifications),具體如何監聽 redis 的過期事件簡單介紹下:org.springframework.session.data.redis.config.ConfigureNotifyKeyspaceEventsAction 該類配置了相關的過期監聽,並使用 SessionExpiredEvent 事件發放 session 的過期事件。為什么引入 C 類型鍵?keyspace notifications 只會告訴我們哪個鍵過期了,不會告訴我們內容是什么。關鍵就在於如果 session 過期后監聽器可能想要訪問 session 的具體內容,然而自身都過期了,還怎么獲取內容。所以,C 類型鍵存在的意義便是解耦 session 的存儲和 session 的過期,並且使得 server 獲取到過期通知后可以訪問到 session 真實的值。對於用戶來說,C 類型鍵過期后,意味着登錄失效,而對於服務端而言,真正的過期其實是 A 類型鍵過期,這中間會有 5 分鍾的誤差。

一點點想法,擔憂,疑惑

本文大概介紹了 Spring Session 的三種 key 的原因,理清楚其中的邏輯花了不少時間,項目改造正好涉及到相關的緩存值過期這一需求,完全可以參考 Spring Session 的方案。但擔憂也是有的,如果真的只是 1~2 兩分鍾的延遲過期(對應 A 輪改造中遇到的問題),以及 1 分鍾的提前刪除(對應 B 輪改造中的並發問題)其實個人感覺沒必要計較。從產品體驗上來說,用戶應該不會在意 32 分鍾自動退出和 30 分鍾退出,可以說 Spring Session 是為了嚴謹而設計了這一套方案,但引入了定時器和很多輔助的鍵值對,無疑對內存消耗和 cpu 消耗都是一種浪費。如果在生產環境大量使用 Spring Session,最好權衡下本文提及的相關問題。

 

https://www.cnkirito.moe/spring-session-4/

(This applies to Spring 1.5.x at the time of this writing)

To add to @radrocket81's reply, here's an example code.
Also this is how you set the max-age and other properties of Spring boot cookies if you enabled Redis session by @EnableRedisHttpSession as application property server.session won't be applied.

@Bean
public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter(SessionRepository<S> sessionRepository, ServletContext servletContext) {
    SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>(sessionRepository);
    sessionRepositoryFilter.setServletContext(servletContext);
    CookieHttpSessionStrategy httpSessionStrategy = new CookieHttpSessionStrategy();
    httpSessionStrategy.setCookieSerializer(this.cookieSerializer());
    sessionRepositoryFilter.setHttpSessionStrategy(httpSessionStrategy);
    return sessionRepositoryFilter;
}

private CookieSerializer cookieSerializer() {
    DefaultCookieSerializer serializer = new DefaultCookieSerializer();
    serializer.setCookieName("CUSTOM_SESSION_KEY");
    serializer.setDomainName("domain.com");
    serializer.setCookiePath("/");
    serializer.setCookieMaxAge(10); //Set the cookie max age in seconds, e.g. 10 seconds
    return serializer;
}

https://stackoverflow.com/questions/34941350/how-to-set-cookie-domain-and-path-with-spring-boot

SpringSession系列-sessionId解析和Cookie讀寫策略
sessionId 解析策略
HttpSessionIdResolver
基於Cookie解析sessionId
基於請求頭解析sessionId
Cookie 序列化策略
CookieValue
Cookie 回寫
jvm_router的處理
參考
首先需求在這里說明下,SpringSession的版本迭代的過程中肯定會伴隨着一些類的移除和一些類的加入,目前本系列使用的版本是github上對象的master的代碼流版本。如果有同學對其他版本中的一些類或者處理有疑惑,歡迎交流。

本篇將來介紹下SpringSession中兩種sessionId解析的策略,這個在之前的文章中其實是有提到過的,這里再拿出來和SpringSession中Cookie相關策略一起學習
下。

sessionId 解析策略
SpringSession中對於sessionId的解析相關的策略是通過HttpSessionIdResolver這個接口來體現的。HttpSessionIdResolver有兩個實現類:

 


這兩個類就分別對應SpringSession解析sessionId的兩種不同的實現策略。再深入了解不同策略的實現細節之前,先來看下HttpSessionIdResolver接口定義的一些行為有哪些。

HttpSessionIdResolver
HttpSessionIdResolver定義了sessionId解析策略的契約(Contract)。允許通過請求解析sessionId,並通過響應發送sessionId或終止會話。接口定義如下:

public interface HttpSessionIdResolver {
List<String> resolveSessionIds(HttpServletRequest request);
void setSessionId(HttpServletRequest request, HttpServletResponse response,String sessionId);
void expireSession(HttpServletRequest request, HttpServletResponse response);
}

HttpSessionIdResolver中有三個方法:

resolveSessionIds:解析與當前請求相關聯的sessionId。sessionId可能來自Cookie或請求頭。
setSessionId:將給定的sessionId發送給客戶端。這個方法是在創建一個新session時被調用,並告知客戶端新sessionId是什么。
expireSession:指示客戶端結束當前session。當session無效時調用此方法,並應通知客戶端sessionId不再有效。比如,它可能刪除一個包含sessionId的Cookie,或者設置一個HTTP響應頭,其值為空就表示客戶端不再提交sessionId。
下面就針對上面提到的兩種策略來進行詳細的分析。

基於Cookie解析sessionId
這種策略對應的實現類是CookieHttpSessionIdResolver,通過從Cookie中獲取session;具體來說,這個實現將允許使用CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)指定Cookie序列化策略。默認的Cookie名稱是“SESSION”。創建一個session時,HTTP響應中將會攜帶一個指定 Cookie name且value是sessionId的Cookie。Cookie 將被標記為一個 session cookie,Cookie 的 domain path 使用 context path,且被標記為HttpOnly,如果HttpServletRequest#isSecure()返回true,那么Cookie將標記為安全的。如下:

關於Cookie,可以參考:聊一聊session和cookie。

HTTP/1.1 200 OK
Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly

這個時候,客戶端應該通過在每個請求中指定相同的Cookie來包含session信息。例如:

GET /messages/ HTTP/1.1
Host: example.com
Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6

當會話無效時,服務器將發送過期的HTTP響應Cookie,例如:

HTTP/1.1 200 OK
Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly

CookieHttpSessionIdResolver 類的實現如下:

public final class CookieHttpSessionIdResolver implements HttpSessionIdResolver {
private static final String WRITTEN_SESSION_ID_ATTR = CookieHttpSessionIdResolver.class
.getName().concat(".WRITTEN_SESSION_ID_ATTR");
// Cookie序列化策略,默認是 DefaultCookieSerializer
private CookieSerializer cookieSerializer = new DefaultCookieSerializer();

@Override
public List<String> resolveSessionIds(HttpServletRequest request) {
// 根據提供的cookieSerializer從請求中獲取sessionId
return this.cookieSerializer.readCookieValues(request);
}

@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response,
String sessionId) {
if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
return;
}
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
// 根據提供的cookieSerializer將sessionId回寫到cookie中
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, sessionId));
}

@Override
public void expireSession(HttpServletRequest request, HttpServletResponse response) {
// 這里因為是過期,所以回寫的sessionId的值是“”,當請求下次進來時,就會取不到sessionId,也就意味着當前會話失效了
this.cookieSerializer.writeCookieValue(new CookieValue(request, response, ""));
}

// 指定Cookie序列化的方式
public void setCookieSerializer(CookieSerializer cookieSerializer) {
if (cookieSerializer == null) {
throw new IllegalArgumentException("cookieSerializer cannot be null");
}
this.cookieSerializer = cookieSerializer;
}
}

這里可以看到CookieHttpSessionIdResolver 中的讀取操作都是圍繞CookieSerializer來完成的。CookieSerializer 是SpringSession中對於Cookie操作提供的一種機制。下面細說。

基於請求頭解析sessionId
這種策略對應的實現類是HeaderHttpSessionIdResolver,通過從請求頭header中解析出sessionId。具體地說,這個實現將允許使用HeaderHttpSessionIdResolver(String)來指定頭名稱。還可以使用便利的工廠方法來創建使用公共頭名稱(例如“X-Auth-Token”和“authenticing-info”)的實例。創建會話時,HTTP響應將具有指定名稱和sessionId值的響應頭。

// 使用X-Auth-Token作為headerName
public static HeaderHttpSessionIdResolver xAuthToken() {
return new HeaderHttpSessionIdResolver(HEADER_X_AUTH_TOKEN);
}
// 使用Authentication-Info作為headerName
public static HeaderHttpSessionIdResolver authenticationInfo() {
return new HeaderHttpSessionIdResolver(HEADER_AUTHENTICATION_INFO);
}

HeaderHttpSessionIdResolver在處理sessionId上相比較於CookieHttpSessionIdResolver來說簡單很多。就是圍繞request.getHeader(String)和request.setHeader(String,String)
兩個方法來玩的。

HeaderHttpSessionIdResolver這種策略通常會在無線端來使用,以彌補對於無Cookie場景的支持。

Cookie 序列化策略
基於Cookie解析sessionId的實現類CookieHttpSessionIdResolver 中實際對於Cookie的讀寫操作都是通過CookieSerializer來完成的。SpringSession 提供了CookieSerializer接口的默認實現DefaultCookieSerializer,當然在實際應用中,我們也可以自己實現這個接口,然后通過CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)方法來指定我們自己的實現方式。

PS:不得不說,強大的用戶擴展能力真的是Spring家族的優良家風。

篇幅有限,這里就只看下兩個點:

CookieValue 存在的意義是什么
DefaultCookieSerializer回寫Cookie的的具體實現,讀Cookie在 SpringSession系列-請求與響應重寫 這篇文章中有介紹過,這里不再贅述。
jvm_router的處理
CookieValue
CookieValue是CookieSerializer中的內部類,封裝了向HttpServletResponse寫入所需的所有信息。其實CookieValue的存在並沒有什么特殊的意義,個人覺得作者一開始只是想通過CookieValue的封裝來簡化回寫cookie鏈路中的參數傳遞的問題,但是實際上貌似並沒有什么減少多少工作量。

Cookie 回寫
Cookie 回寫我覺得對於分布式session的實現來說是必不可少的;基於標准servlet實現的HttpSession,我們在使用時實際上是不用關心回寫cookie這個事情的,因為servlet容器都已經做了。但是對於分布式session來說,由於重寫了response,所以需要在返回response時需要將當前session信息通過cookie的方式塞到response中返回給客戶端-這就是Cookie回寫。下面是DefaultCookieSerializer中回寫Cookie的邏輯,細節在代碼中通過注釋標注出來。

@Override
public void writeCookieValue(CookieValue cookieValue) {
HttpServletRequest request = cookieValue.getRequest();
HttpServletResponse response = cookieValue.getResponse();
StringBuilder sb = new StringBuilder();
sb.append(this.cookieName).append('=');
String value = getValue(cookieValue);
if (value != null && value.length() > 0) {
validateValue(value);
sb.append(value);
}
int maxAge = getMaxAge(cookieValue);
if (maxAge > -1) {
sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
OffsetDateTime expires = (maxAge != 0)
? OffsetDateTime.now().plusSeconds(maxAge)
: Instant.EPOCH.atOffset(ZoneOffset.UTC);
sb.append("; Expires=")
.append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
}
String domain = getDomainName(request);
if (domain != null && domain.length() > 0) {
validateDomain(domain);
sb.append("; Domain=").append(domain);
}
String path = getCookiePath(request);
if (path != null && path.length() > 0) {
validatePath(path);
sb.append("; Path=").append(path);
}
if (isSecureCookie(request)) {
sb.append("; Secure");
}
if (this.useHttpOnlyCookie) {
sb.append("; HttpOnly");
}
if (this.sameSite != null) {
sb.append("; SameSite=").append(this.sameSite);
}

response.addHeader("Set-Cookie", sb.toString());
}

這上面就是拼湊字符串,然后塞到Header里面去,最終再瀏覽器中顯示大體如下:

Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly
1
jvm_router的處理
在Cookie的讀寫代碼中都涉及到對於jvmRoute這個屬性的判斷及對應的處理邏輯。

1、讀取Cookie中的代碼片段

if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) {
sessionId = sessionId.substring(0,
sessionId.length() - this.jvmRoute.length());
}

2、回寫Cookie中的代碼片段

if (this.jvmRoute != null) {
actualCookieValue = requestedCookieValue + this.jvmRoute;
}

jvm_route是Nginx中的一個模塊,其作用是通過session cookie的方式來獲取session粘性。如果在cookie和url中並沒有session,則這只是個簡單的 round-robin 負載均衡。其具體過程分為以下幾步:

1.第一個請求過來,沒有帶session信息,jvm_route就根據round robin策略發到一台tomcat上面。
2.tomcat添加上 session 信息,並返回給客戶。
3.用戶再次請求,jvm_route看到session中有后端服務器的名稱,它就把請求轉到對應的服務器上。
從本質上來說,jvm_route也是解決session共享的一種解決方式。這種和 SpringSession系列-分布式Session實現方案 中提到的基於IP-HASH的方式有點類似。那么同樣,這里存在的問題是無法解決宕機后session數據轉移的問題,既宕機就丟失。

DefaultCookieSerializer 中除了Cookie的讀寫之后,還有一些細節也值得關注下,比如對Cookie中值的驗證、remember-me的實現等。

參考
SpringSession官方文檔
jvm_router原理
SpringSession中文注釋持續更新代碼分支
---------------------
作者:sinat_25518349
來源:CSDN
原文:https://blog.csdn.net/sinat_25518349/article/details/85042029
版權聲明:本文為博主原創文章,轉載請附上博文鏈接!

https://stackoverflow.com/questions/33095345/how-to-change-spring-session-redis-cookie-name

spring-session+jedis中session的過期時間和內置tomcat的處理方式的區別

  

 http://www.voidcn.com/article/p-szwnbyjs-brm.html

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM