摘要:本文主要研究基於 spring-seesion 解決分佈式 session 的共享問題。首先聊一下session與cookie的做用與工做原理,而後步入主題,講述session 共享問題的產生背景以及常見的解決方案;接着講述了 spring-session 的兩種管理 sessionid 的方式以及對應的使用場景;再接着對後臺保存數據到 redis 上的數據結構進行了分析;而後對 spring-session 的核心源代碼進行了解讀,方便理解 spring-session 框架的實現原理。
html
由於HTTP協議是無狀態的,即服務器不知道用戶上一次作了什麼,沒法建立同一用戶請求的關聯性,所以須要瀏覽器提供一個機制供服務端識別,這時Cookie便出現了。 經過引入cookie和session體系機制來維護狀態信息。即用戶第一次訪問服務器的時候,服務器響應報頭一般會出現一個Set-Cookie響應頭,這裏其實就是在本地設置一個cookie,當用戶再次訪問服務器的時候,http會附帶這個cookie過去,cookie中存有sessionId這樣的信息來到服務器這邊確認是否屬於同一次會話。web
name:cookie的名字,Cookie一旦建立,名稱便不可更改redis
value:cookie值spring
comment:該Cookie的用處說明。瀏覽器顯示Cookie信息的時候顯示該說明。數據庫
domain:能夠訪問該Cookie的域名。若是設置爲「.baidu.com」,則全部以「baidu.com」結尾的域名均可以訪問該Cookie;也就是隻有一級域名一致的狀況下才能夠訪問同一cookie。後端
maxAge:Cookie失效的時間,單位秒。跨域
正數,則超過maxAge秒以後失效。 負數,該Cookie爲臨時Cookie,關閉瀏覽器即失效,瀏覽器也不會以任何形式保存該Cookie。 爲0,表示刪除該Cookie。 path:該Cookie的使用路徑。例如:瀏覽器
path=/,說明本域名下contextPath均可以訪問該Cookie。tomcat
path=/app/,則只有contextPath爲「/app」的程序能夠訪問該Cookie。安全
path設置時,其以「/」結尾。
secure: 該Cookie是否僅被使用安全協議傳輸。這裏的安全協議包括HTTPS,SSL等。默認爲false。
Cookie是不支持跨域的,對於Cookie來講,Cookie的同源只關注域名,是忽略協議和端口的。因此通常狀況下,https://localhost:80/和http://localhost:8080/的Cookie是共享的。
Cookie是不可跨域的;在沒有通過任何處理的狀況下,二級域名不一樣也是不行的。(wenku.baidu.com和baike.baidu.com)。只有當domainname設置爲.baidu.com時才能夠訪問同一cookie。
Cookie數量&大小限制及處理策略 www.cnblogs.com/henryhappie…
2.Session
Cookie機制彌補了HTTP協議無狀態的不足。在Session出現以前,基本上全部的網站都採用Cookie來跟蹤會話。 與Cookie不一樣的是,session是以服務端保存狀態的。
當客戶端請求建立一個session的時候,服務器會先檢查這個客戶端的請求裏是否已包含了一個session標識 - sessionId,
若是已包含這個sessionId,則說明之前已經爲此客戶端建立過session,服務器就按照sessionId把這個session檢索出來使用(若是檢索不到,可能會新建一個) 若是客戶端請求不包含sessionId,則爲此客戶端建立一個session而且生成一個與此session相關聯的sessionId
sessionId的值通常是一個既不會重複,又不容易被仿造的字符串,這個sessionId將被在本次響應中返回給客戶端保存。保存sessionId的方式大多狀況下用的是cookie。
擴展:session的生命週期
session建立:在第一次使用resquest的getSession方法,web服務器會建立一個session
session使用:session在服務端建立完成後,內存會給session分配必定的空間,而且會生成一個臨時cookie返回給用戶,瀏覽器經過set-cookie建立cookie並保存到本地,此後訪問都經過此cookieid找到對應的session。
session的銷燬:
1.默認銷燬:若是與服務端30分鐘內沒有交互,默認銷燬。
2.手動銷燬:當調用session的invalidate方法時候會銷燬此session。
3.關閉服務器:內存空間被回收了,天然就不存在session了。
複製代碼
解決方案-SpringSession
HttpSession 是經過 Servlet 容器建立和管理的,像 Tomcat/Jetty 都是保存在內存中的。而若是咱們把 web 服務器搭建成分佈式的集羣,而後利用Nginx 作負載均衡,那麼來自同一用戶的 Http 請求將有可能被分發到兩個不一樣的 web 站點中去。那麼問題就來了,如何保證不一樣的 web 站點可以共享同一份 session 數據呢?
最簡單的作法是把session從容器中抽離出來。 第一種是使用容器擴展來實現,你們比較容易接受的是經過容器插件來實現,好比基於 Tomcat 的 tomcat-redis-session-manager ,基於 Jetty 的 jetty-session-redis等等。好處是對項目來講是透明的,無需改動代碼。不過前者目前還不支持 Tomcat 8 ,或者說不太完善。可是因爲過於依賴容器,一旦容器升級或者更換意味着又得重新來過。而且代碼不在項目中,對開發者來講維護也是個問題。
第二種是本身寫一套會話管理的工具類,包括 Session 管理和 Cookie 管理,在須要使用會話的時候都從本身的工具類中獲取,而工具類後端存儲能夠放到 Redis 中。很顯然這個方案靈活性最大,但開發須要一些額外的時間。而且系統中存在兩套 Session 方案,很容易弄錯而致使取不到數據。
第三種是使用框架的會話管理工具,也就是以下介紹的 spring-session ,能夠理解是替換了 Servlet 那一套會話管理,接管建立和管理 Session 數據的工做。既不依賴容器,又不須要改動代碼,而且是用了 spring-data-redis 那一套鏈接池,能夠說是最完美的解決方案。固然除了redis管理存儲外,spring-session也可經過數據庫經過jdbc存儲
1.spring Session 提供了 API 和實現,用於管理用戶的 Session 信息。除此以外,它還提供了以下特性:
2.將 session 所保存的狀態卸載到特定的外部 session 存儲彙總,如 Redis 中,他們可以以獨立於應用服務器的方式提供高質量的集羣。
3.控制 sessionid 如何在客戶端和服務器之間進行交換,這樣的話就能很容易地編寫 Restful API ,由於它能夠從 HTTP 頭信息中獲取 sessionid ,而沒必要再依賴於 cookie。
4.在非 Web 請求的處理代碼中,可以訪問 session 數據,好比在 JMS 消息的處理代碼中。
5.支持每一個瀏覽器上使用多個 session,從而可以很容易地構建更加豐富的終端用戶體驗。
當用戶使用 WebSocket 發送請求的時候,可以保持 HttpSession 處於活躍狀態。
方案一:由 cookie 管理 sessionid(默認管理方式) 在springboot中集成springsession很是簡單,引入
因爲spring-session默認採用cookie管理策略,因此使用spring-session只須要在啓動類添加@EnableRedisHttpSession註解,參數對應可設置session過時時間,以及redis存放空間位置,刷新模式以及定時清除。 maxInactiveIntervalInSeconds - 會話將在幾秒鐘後到期的時間
redisNamespace - 容許爲會話配置特定於應用程序的命名空間。 Redis鍵和通道ID將以 spring:session:: 的前綴開頭。
redisFlushMode - 容許指定什麼時候將數據寫入Redis。默認值僅在 SessionRepository 上調用 save 時。值 RedisFlushMode.IMMEDIATE 將盡快寫入Redis。
SpringSession 提供了CookieSerializer接口的默認實現DefaultCookieSerializer,固然在實際應用中,咱們也能夠本身實現這個接口,而後經過CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)方法來指定咱們本身的實現方式。
方案二:經過HttpHeader管理sessionid
spring-session支持請求頭來管理session,當cookie被禁用的狀況下能夠經過請求頭攜帶token來匹配對應session。Spring Session容許在 Headers 中提供會話ID以使用 RESTful APIs
下面就是如何使用這兩種管理方式: SpringSession中對於sessionId的解析相關的策略是經過HttpSessionIdResolver這個接口來體現的。HttpSessionIdResolver有兩個實現類:
`public interface HttpSessionIdResolver {List<String> resolveSessionIds(HttpServletRequest request);
void setSessionId(HttpServletRequest request, HttpServletResponse response,String sessionId);
void expireSession(HttpServletRequest request, HttpServletResponse response);
複製代碼
}
resolveSessionIds:解析與當前請求相關聯的sessionId。sessionId可能來自Cookie或請求頭。
setSessionId:將給定的sessionId發送給客戶端。這個方法是在建立一個新session時被調用,並告知客戶端新sessionId是什麼。
expireSession:指示客戶端結束當前session。當session無效時調用此方法,並應通知客戶端sessionId再也不有效。好比,它可能刪除一個包含sessionId的Cookie,或者設置一個HTTP響應頭,其值爲空就表示客戶端再也不提交sessionId。
咱們能夠經過建立HttpSessionIdResolver的自定義實現類來選擇合適的session管理策略。
SpringSession源碼解析
在這裏spring-session是經過redis來管理的,若是須要了解redis是如何操做的,就須要瞭解一下RedisOperationsSessionRepository這個類了。
RedisOperationsSessionRepository 是使用Spring Data的 RedisOperations 實現的 SessionRepository 。在Web環境中,這一般與 SessionRepositoryFilter 結合使用。該實現支持 SessionDestroyedEvent 和 SessionCreatedEvent 至 SessionMessageListener 。
Spring Session使用 Session 的最基本的API是 SessionRepository 。這個API有意很是簡單,所以很容易提供具備基本功能的其餘實現。 一些 SessionRepository 實現也能夠選擇實現 FindByIndexNameSessionRepository 。例如,Spring的Redis支持實現 FindByIndexNameSessionRepository 。 FindByIndexNameSessionRepository 添加了一種方法來查找特定用戶的全部會話。這是經過確保使用用戶名填充名稱爲 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 的會話屬性來完成的。開發人員有責任確保填充屬性,由於Spring Session不知道正在使用的身份驗證機制。下面是一個如何使用它的示例:
String username = "username"; this.session.setAttribute( FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username);
FindByIndexNameSessionRepository 的某些實現將提供鉤子以自動索引其餘會話屬性。對於例如,許多實現將自動確保使用索引名稱 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 索引當前的Spring Security用戶名。
String username = "username";
Map<String, Session> sessionIdToSession = this.sessionRepository .findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,username);