Session和Cookie這兩個概念,在學習java web開發之初,大多數人就已經接觸過了。最近在研究跨域單點登陸的實現時,發現對於Session和Cookie的瞭解,並非很深刻,因此打算寫兩篇文章記錄一下本身的理解。在咱們的應用集成Spring Session以前,先補充一點Session和Cookie的關鍵知識。javascript
Session與Cookie基礎
因爲http協議是無狀態的協議,爲了可以記住請求的狀態,因而引入了Session和Cookie的機制。咱們應該有一個很明確的概念,那就是Session是存在於服務器端的,在單體式應用中,他是由tomcat管理的,存在於tomcat的內存中,當咱們爲了解決分佈式場景中的session共享問題時,引入了redis,其共享內存,以及支持key自動過時的特性,很是契合session的特性,咱們在企業開發中最經常使用的也就是這種模式。可是隻要你願意,也能夠選擇存儲在JDBC,Mongo中,這些,spring都提供了默認的實現,在大多數狀況下,咱們只須要引入配置便可。而Cookie則是存在於客戶端,更方便理解的說法,能夠說存在於瀏覽器。Cookie並不經常使用,至少在我不長的web開發生涯中,並無什麼場景須要我過多的關注Cookie。http協議容許從服務器返回Response時攜帶一些Cookie,而且同一個域下對Cookie的數量有所限制,以前說過Session的持久化依賴於服務端的策略,而Cookie的持久化則是依賴於本地文件。雖說Cookie並不經常使用,可是有一類特殊的Cookie倒是咱們須要額外關注的,那即是與Session相關的sessionId,他是真正維繫客戶端和服務端的橋樑。php
代碼示例
用戶發起請求,服務器響應請求,並作一些用戶信息的處理,隨後返回響應給用戶;用戶再次發起請求,攜帶sessionId,服務器便可以識別,這個用戶就是以前請求的那個。html
使用Springboot編寫一個很是簡單的服務端,來加深對其的理解。需求很簡單,當瀏覽器訪問localhost:8080/test/cookie?browser=xxx
時,若是沒有獲取到session,則將request中的browser存入session;若是獲取到session,便將session中的browser值輸出。順便將request中的全部cookie打印出來。前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
@Controller public class CookieController {
@RequestMapping("/test/cookie") public String cookie(@RequestParam("browser") String browser, HttpServletRequest request, HttpSession session) { |
咱們沒有引入其餘任何依賴,看看原生的session機制是什麼。html5
1 使用chrome瀏覽器,訪問localhost:8080/test/cookie?browser=chrome
,控制檯輸出以下:java
1
|
Session Info: 不存在session,設置browser=chrome
|
既沒有session,也沒有cookie,咱們將browser=chrome設置到session中。nginx
再次訪問一樣的端點,控制檯輸出以下:git
1 2
|
Session Info: 存在session,browser=chrome Cookie Info: JSESSIONID : 4CD1D96E04FC390EA6C60E8C40A636AF
|
屢次訪問以後,控制檯依舊打印出一樣的信息。github
稍微解讀下這個現象,能夠驗證一些結論。當服務端往session中保存一些數據時,Response中自動添加了一個Cookie:JSESSIONID:xxxx,再後續的請求中,瀏覽器也是自動的帶上了這個Cookie,服務端根據Cookie中的JSESSIONID取到了對應的session。這驗證了一開始的說法,客戶端服務端是經過JSESSIONID進行交互的,而且,添加和攜帶key爲JSESSIONID的Cookie都是tomcat和瀏覽器自動幫助咱們完成的,這很關鍵。web
2 使用360瀏覽器,訪問localhost:8080/test/cookie?browser=360
第一次訪問:
1
|
Session Info: 不存在session,設置browser=360
|
後續訪問:
1 2
|
Session Info: 存在session,browser=360 Cookie Info: JSESSIONID : 320C21A645A160C4843D076204DA2F40
|
爲何要再次使用另外一個瀏覽器訪問呢?先賣個關子,咱們最起碼能夠得出結論,不一樣瀏覽器,訪問是隔離的,甚至從新打開同一個瀏覽器,JSESSIONID也是不一樣的。另外能夠嘗試把保存session的操做注視掉,則能夠發現Response中就不會返回JSESSIONID了,即這是一次無狀態的請求。
安全問題
其實上述的知識點,都是很是淺顯的,之因此囉嗦一句,是爲了引出這一節的內容,以及方便觀察後續咱們引入Spring Session以後的發生的變化。
還記得上一節的代碼示例中,咱們使用了兩個瀏覽器:
- chrome瀏覽器訪問時,JSESSIONID爲4CD1D96E04FC390EA6C60E8C40A636AF,後端session記錄的值爲:browser=chrome。
- 360瀏覽器訪問時,JSESSIONID爲320C21A645A160C4843D076204DA2F40,後端session記錄的值爲:browser=360。
咱們使用chrome插件Edit this Cookie,將chrome瀏覽器中的JSESSIONID修改成360瀏覽器中的值
EditThisCookie
一樣訪問原來的端點:localhost:8080/test/cookie?browser=chrome,獲得的輸出以下:
1 2
|
存在session,browser=360 JSESSIONID : 320C21A645A160C4843D076204DA2F40
|
證明了一點,存放在客戶端的Cookie的確是存在安全問題的,咱們使用360的JSESSIONID「騙」過了服務器。畢竟,服務器只能經過Cookie中的JSESSIONID來辨別身份。(這提示咱們不要在公共場合保存Cookie信息,如今的瀏覽器在保存Cookie時一般會讓你肯定一次)
下一篇文章,將正式講解如何在應用中集成Spring Session。
https://www.cnkirito.moe/spring-session-1/
1. Spring Java Configuration
Once you have set up Spring Session, you can customize how the session cookie is written by exposing a CookieSerializer
as a Spring bean. Spring Session comes with DefaultCookieSerializer
. Exposing the DefaultCookieSerializer
as a Spring bean augments the existing configuration when you use configurations like @EnableRedisHttpSession
. The following example shows how to customize Spring Session’s cookie:
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$");
return serializer;
}
|
We customize the name of the cookie to be JSESSIONID . |
|
We customize the path of the cookie to be / (rather than the default of the context root). |
|
We customize the domain name pattern (a regular expression) to be ^.?\\.(\\w\\.[a-z]+)$ . This allows sharing a session across domains and applications. If the regular expression does not match, no domain is set and the existing domain is used. If the regular expression matches, the first grouping is used as the domain. This means that a request to https://child.example.com sets the domain to example.com . However, a request to http://localhost:8080/ or https://192.168.1.100:8080/ leaves the cookie unset and, thus, still works in development without any changes being necessary for production. |
https://docs.spring.io/spring-session/docs/current/reference/html5/guides/java-custom-cookie.html
前言
在開始spring-session揭祕以前,先作下熱腦(活動活動腦子)運動。主要從如下三個方面進行熱腦:
- 爲何要spring-session
- 比較traditional-session方案和spring-session方案
- JSR340規範與spring-session的透明繼承
一.爲何要spring-session
在傳統單機web應用中,通常使用tomcat/jetty等web容器時,用戶的session都是由容器管理。瀏覽器使用cookie中記錄sessionId,容器根據sessionId判斷用戶是否存在會話session。這裏的限制是,session存儲在web容器中,被單臺服務器容器管理。
可是網站主鍵演變,分佈式應用和集羣是趨勢(提升性能)。此時用戶的請求可能被負載分發至不一樣的服務器,此時傳統的web容器管理用戶會話session的方式即行不通。除非集羣或者分佈式web應用可以共享session,儘管tomcat等支持這樣作。可是這樣存在如下兩點問題:
- 須要侵入web容器,提升問題的複雜
- web容器之間共享session,集羣機器之間勢必要交互耦合
基於這些,必須提供新的可靠的集羣分佈式/集羣session的解決方案,突破traditional-session單機限制(即web容器session方式,下面簡稱traditional-session),spring-session應用而生。
二.比較traditional-session方案和spring-session方案
下圖展現了traditional-session和spring-session的區別
傳統模式中,當request進入web容器,根據reqest獲取session時,若是web容器中存在session則返回,若是不存在,web容器則建立一個session。而後返回response時,將sessonId做爲response的head一併返回給客戶端或者瀏覽器。
可是上節中說明了traditional-session的侷限性在於:單機session。在此限制的相反面,即將session從web容器中抽出來,造成獨立的模塊,以便分佈式應用或者集羣都能共享,即能解決。
spring-session的核心思想在於此:將session從web容器中剝離,存儲在獨立的存儲服務器中。目前支持多種形式的session存儲器:Redis、Database、MogonDB等。session的管理責任委託給spring-session承擔。當request進入web容器,根據request獲取session時,由spring-session負責存存儲器中獲取session,若是存在則返回,若是不存在則建立並持久化至存儲器中。
三.JSR340規範與spring-session的透明繼承
JSR340是Java Servlet 3.1的規範提案,其中定義了大量的api,包括:servlet、servletRequest/HttpServletRequest/HttpServletRequestWrapper、servletResponse/HttpServletResponse/HttpServletResponseWrapper、Filter、Session等,是標準的web容器須要遵循的規約,如tomcat/jetty/weblogic等等。
在平常的應用開發中,develpers也在頻繁的使用servlet-api,好比:
如下的方式獲取請求的session:
HttpServletRequest request = ... HttpSession session = request.getSession(false);
其中HttpServletRequest和HttpSession都是servlet規範中定義的接口,web容器實現的標準。那若是引入spring-session,要如何獲取session?
- 遵循servlet規範,一樣方式獲取session,對應用代碼無侵入且對於developers透明化
- 全新實現一套session規範,定義一套新的api和session管理機制
兩種方案均可以實現,可是顯然第一種更友好,且具備兼容性。spring-session正是第一種方案的實現。
實現第一種方案的關鍵點在於作到透明和兼容
- 接口適配:仍然使用HttpServletRequest獲取session,獲取到的session仍然是HttpSession類型——適配器模式
- 類型包裝加強:Session不能存儲在web容器內,要外化存儲——裝飾模式
讓人興奮的是,以上的需求在Servlet規範中的擴展性都是予以支持!Servlet規範中定義一系列的接口都是支持擴展,同時提供Filter支撐擴展點。建議閱讀《JavaTM Servlet Specification》。
熱腦活動結束,下面章節正式進入今天的主題:spring-session揭祕
Spring Session探索
主要從如下兩個方面來講spring-session:
一.特色
spring-session在無需綁定web容器的狀況下提供對集羣session的支持。並提供對如下狀況的透明集成:
- HttpSession:允許替換web容器的HttpSession
- WebSocket:使用WebSocket通訊時,提供Session的活躍
- WebSession:允許以應用中立的方式替換webflux的webSession
二.工做原理
再詳細閱讀源碼以前先來看張圖,介紹下spring-session中的核心模塊以及之間的交互。
spring-session分爲如下核心模塊:
- SessionRepositoryFilter:Servlet規範中Filter的實現,用來切換HttpSession至Spring Session,包裝HttpServletRequest和HttpServletResponse
- HttpServerletRequest/HttpServletResponse/HttpSessionWrapper包裝器:包裝原有的HttpServletRequest、HttpServletResponse和Spring Session,實現切換Session和透明繼承HttpSession的關鍵之所在
- Session:Spring Session模塊
- SessionRepository:管理Spring Session的模塊
- HttpSessionStrategy:映射HttpRequst和HttpResponse到Session的策略
1. SessionRepositoryFilter
SessionRepositoryFilter是一個Filter過濾器,符合Servlet的規範定義,用來修改包裝請求和響應。這裏負責包裝切換HttpSession至Spring Session的請求和響應。
@Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
以上是SessionRepositoryFilter的核心操做,每一個HttpRequest進入,都會被該Filter包裝成切換Session的請求很響應對象。
Tips:責任鏈模式
Filter是Servlet規範中的很是重要的組件,在tomcat的實現中使用了責任鏈模式,將多個Filter組織成鏈式調用。Filter的做用就是在業務邏輯執行先後對請求和響應作修改配置。配合HttpServletRequestWrapper和HttpServletResponseWrapper使用,可謂威力驚人!
2. SessionRepositoryRequestWrapper
對於developers獲取HttpSession的api
HttpServletRequest request = ...; HttpSession session = request.getSession(true);
在spring session中request的實際類型SessionRepositoryRequestWrapper。調用SessionRepositoryRequestWrapper的getSession方法會觸發建立spring session,而非web容器的HttpSession。
SessionRepositoryRequestWrapper用來包裝原始的HttpServletRequest實現HttpSession切換至Spring Session。是透明Spring Session透明集成HttpSession的關鍵。
private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper { private final String CURRENT_SESSION_ATTR = HttpServletRequestWrapper.class .getName();
SessionRepositoryRequestWrapper繼承Servlet規範中定義的包裝器HttpServletRequestWrapper。HttpServletRequestWrapper是Servlet規範api提供的用於擴展HttpServletRequest的擴張點——即裝飾器模式,能夠經過重寫一些api達到功能點的加強和自定義。
Tips:裝飾器模式
裝飾器模式(包裝模式)是對功能加強的一種絕佳模式。實際利用的是面向對象的多態性實現擴展。Servlet規範中開放此HttpServletRequestWrapper接口,是讓developers自行擴展實現。這種使用方式和jdk中的FilterInputStream/FilterInputStream一模一樣。
HttpServletRequestWrapper中持有一個HttpServletRequest對象,而後實現HttpServletRequest接口的全部方法,全部方法實現中都是調用持有的HttpServletRequest對象的相應的方法。繼承HttpServletRequestWrapper 能夠對其重寫。SessionRepositoryRequestWrapper繼承HttpServletRequestWrapper,在構造方法中將原有的HttpServletRequest經過調用super完成對HttpServletRequestWrapper中持有的HttpServletRequest初始化賦值,而後重寫和session相關的方法。這樣就保證SessionRepositoryRequestWrapper的其餘方法調用都是使用原有的HttpServletRequest的數據,只有session相關的是重寫的邏輯。
Tips:
這裏的設計是否很精妙!一切都多虧與Servlet規範設計的的巧妙啊!
@Override public HttpSessionWrapper getSession() { return getSession(true); }
重寫HttpServletRequest的getSession()方法,調用有參數getSession(arg)方法,默認爲true,表示當前reques沒有session時建立session。繼續看下有參數getSession(arg)的重寫邏輯.
@Override public HttpSessionWrapper getSession(boolean create) {
再來看下spring session的持久化。上述SessionRepositoryFilter在包裝HttpServletRequest後,執行FilterChain中使用finally保證請求的Session始終session會被提交,此提交操做中將sesionId設置到response的head中並將session持久化至存儲器中。
持久化只持久spring session,並非將spring session包裝後的HttpSession持久化,由於HttpSession不過是包裝器,持久化沒有意義。
再來看下包裝的響應SessionRepositoryResponseWrapper。
3.SessionRepositoryResponseWrapper
上面的註釋已經很是詳細,這裏再也不贅述。這裏只講述爲何須要包裝原始的響應。從註釋上能夠看出包裝響應時爲了:確保若是響應被提交session可以被保存。
這裏我有點疑惑:在上述的SessionRepositoryFilter.doFilterInternal方法中不是已經request.commitSession()了嗎,FilterChain執行完或者異常後都會執行Finally中的request.commitSession。爲何這裏仍然須要包裝響應,爲了確保session可以保存,包裝器中的onResponseCommitted方法能夠看出也是作了一次request.commitSession()。難道這不是畫蛇添足?
Tips
若是有和我相同疑問的同窗,那就說明咱們的基礎都不紮實,對Servlet仍然沒有一個清楚全面的認識。對於此問題,我特地在github上提了issuse:Why is the request.commitSession() method called repeatedly?。
可是在提完issue後的回家路上,我思考了下response能夠有流方式的寫,會不會在response.getOutStream寫的時候已經將響應所有返回到客戶端,這時響應結束。
在家中是,spring sesion做者大大已經回覆了個人issue:
Is this causing you problems? The reason is that we need to ensure that the session is created before the response is committed. If the response is already committed there will be no way to track the session (i.e. a cookie cannot be written to the response to keep track of which session id).
他的意思是:咱們須要在response被提交以前確保session被建立。若是response已經被提交,將沒有辦法追蹤session(例如:沒法將cookie寫入response以跟蹤哪一個session id)。
在此以前我又閱讀了JavaTM Servlet Specification,規範中這樣解釋Response的flushBuffer接口:
The isCommitted method returns a boolean value indicating whether any response bytes have been returned to the client. The flushBuffer method forces content in the buffer to be written to the client.
而且看了ServletResponse的flushBuffer的javadocs:
結合以上兩點,一旦response執行flushBuffer方法,迫使Response中在Buffer中任何數據都會被返回至client端。這個方法自動提交響應中的status code和head。那麼若是不包裝請求,監聽flushBuffer事件在提交response前,將session寫入response和持久化session,將致使做者大大說的沒法追蹤session。
SessionRepositoryResponseWrapper繼承父類OnCommittedResponseWrapper,其中flushBuffer方法以下:
重寫HttpServletResponse方法,監聽response commit,當發生response commit時,能夠在commit以前寫session至response中並持久化session。
Tips:
spring mvc中HttpMessageConverters使用到的jackson即調用了outstream.flushBuffer(),當使用@ResponseBody時。
以上作法當然合理,可是如此重複操做兩次commit,存在兩次persist session?
這個問題後面涉及SessionRepository時再詳述!
再看SessionRepository以前,先來看下spring session中的session接口。
3.Session接口
spring-session和tomcat中的Session的實現模式上有很大不一樣,tomcat中直接對HttpSession接口進行實現,而spring-session中則抽象出單獨的Session層接口,讓後再使用適配器模式將Session適配層Servlet規範中的HttpSession。spring-sesion中關於session的實現和適配整個UML類圖以下:
Tips:適配器模式
spring-session單獨抽象出Session層接口,能夠應對多種場景下不一樣的session的實現,而後經過適配器模式將Session適配成HttpSession的接口,精妙至極!
Session是spring-session對session的抽象,主要是爲了鑑定用戶,爲Http請求和響應提供上下文過程,該Session能夠被HttpSession、WebSocket Session,非WebSession等使用。定義了Session的基本行爲:
- getId:獲取sessionId
- setAttribute:設置session屬性
- getAttribte:獲取session屬性
ExipringSession:提供Session額外的過時特性。定義瞭如下關於過時的行爲:
- setLastAccessedTime:設置最近Session會話過程當中最近的訪問時間
- getLastAccessedTime:獲取最近的訪問時間
- setMaxInactiveIntervalInSeconds:設置Session的最大閒置時間
- getMaxInactiveIntervalInSeconds:獲取最大閒置時間
- isExpired:判斷Session是否過時
MapSession:基於java.util.Map的ExpiringSession的實現
RedisSession:基於MapSession和Redis的ExpiringSession實現,提供Session的持久化能力
先來看下MapSession的代碼源碼片斷
public final class MapSession implements ExpiringSession, Serializable {
MapSession中持有HashMap類型的變量sessionAtts用於存儲Session設置屬性,好比調用的setAttribute方法的k-v就存儲在該HashMap中。這個和tomcat內部實現HttpSession的方式相似,tomcat中使用了ConcurrentHashMap存儲。
其中lastAccessedTime用於記錄最近的一次訪問時間,maxInactiveInterval用於記錄Session的最大閒置時間(過時時間-針對沒有Request活躍的狀況下的最大時間,即相對於最近一次訪問後的最大閒置時間)。
public void setAttribute(String attributeName, Object attributeValue) { if (attributeValue == null) { removeAttribute(attributeName); } else { this.sessionAttrs.put(attributeName, attributeValue); } }
setAttribute方法極其簡單,null時就移除attributeName,不然put存儲。
重點熟悉RedisSession如何實現Session的行爲:setAttribute、persistence等。
首先看javadocs,對於閱讀源碼,學會看javadocs很是重要!
基於MapSession的基本映射實現的Session,可以追蹤發生變化的全部屬性,當調用saveDelta方法後,變化的屬性將被持久化!
在RedisSession中有兩個很是重要的成員屬性:
- cached:其實是一個MapSession實例,用於作本地緩存,每次在getAttribute時無需從Redis中獲取,主要爲了improve性能
- delta:用於跟蹤變化數據,作持久化
再來看下RedisSession中最爲重要的行爲saveDelta——持久化Session至Redis中:
從javadoc中能夠看出,saveDelta用於存儲Session的屬性:
- 保存Session中的屬性數據至Redis中
- 清空delta中數據,防止重複提交Session中的數據
- 更新過時時間至下一個過時時間間隔的時刻
再看下RedisSession中的其餘行爲
除了MapSession和RedisSession還有JdbcSession、MongoExpiringSession,感興趣的讀者能夠自行閱讀。
下面看SessionRepository的邏輯。SessionRepository是spring session中用於管理spring session的核心組件。
4. SessionRepository
A repository interface for managing {@link Session} instances.
javadoc中描述SessionRepository爲管理spring-session的接口實例。抽象出:
S createSession(); void save(S session); S getSession(String id); void delete(String id);
建立、保存、獲取、刪除Session的接口行爲。根據Session的不一樣,分爲不少種Session操做倉庫。
這裏重點介紹下RedisOperationsSessionRepository。在詳細介紹其以前,瞭解下RedisOperationsSessionRepository的數據存儲細節。
當建立一個RedisSession,而後存儲在Redis中時,RedisSession的存儲細節以下:
spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000
Redis會爲每一個RedisSession存儲三個k-v。
- 第一個k-v用來存儲Session的詳細信息,包括Session的過時時間間隔、最近的訪問時間、attributes等等。這個k的過時時間爲Session的最大過時時間 + 5分鐘。若是默認的最大過時時間爲30分鐘,則這個k的過時時間爲35分鐘
- 第二個k-v用來表示Session在Redis中的過時,這個k-v不存儲任何有用數據,只是表示Session過時而設置。這個k在Redis中的過時時間即爲Session的過時時間間隔
- 第三個k-v存儲這個Session的id,是一個Set類型的Redis數據結構。這個k中的最後的1439245080000值是一個時間戳,根據這個Session過時時刻滾動至下一分鐘而計算得出。
這裏不禁好奇,爲何一個RedisSession卻如此複雜的存儲。關於這個能夠參考spring-session做者本人在github上的兩篇回答:
Why does Spring Session use spring:session:expirations?
Clarify Redis expirations and cleanup task
簡單描述下,爲何RedisSession的存儲用到了三個Key,而非一個Redis過時Key。
對於Session的實現,須要支持HttpSessionEvent,即Session建立、過時、銷燬等事件。當應用用監聽器設置監聽相應事件,Session發生上述行爲時,監聽器可以作出相應的處理。
Redis的強大之處在於支持KeySpace Notifiction——鍵空間通知。便可以監視某個key的變化,如刪除、更新、過時。當key發生上述行爲是,以即可以接受到變化的通知作出相應的處理。具體詳情能夠參考:
Redis Keyspace Notifications
可是Redis中帶有過時的key有兩種方式:
當訪問時發現其過時,會產生過時事件,可是沒法保證key的過時時間抵達後當即生成過時事件。具體能夠參考:Timing of expired events
spring-session爲了可以及時的產生Session的過時時的過時事件,因此增長了:
spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
spring:session:expirations:1439245080000
spring-session中有個定時任務,每一個整分鐘都會查詢相應的spring:session:expirations:整分鐘的時間戳中的過時SessionId,而後再訪問一次這個SessionId,即spring:session:sessions:expires:SessionId,以便可以讓Redis及時的產生key過時事件——即Session過時事件。
接下來再看下RedisOperationsSessionRepository中的具體實現原理
createSession方法:
public RedisSession createSession() {
再來看下RedisSession的構造方法:
save方法:
public void save(RedisSession session) {
getSession方法:
在有些狀況下,Session過時,仍然須要可以獲取到Session。這裏先來看下getSession(String id, boolean allowExpired):
private RedisSession getSession(String id, boolean allowExpired) {
這裏須要注意的是loaded.isExpired()和loadSession。loaded.isExpired判斷Session是否過時,若是過時返回null:
public boolean isExpired() {
loadSession中,將Redis中存儲的Session信息轉換爲MapSession對象,以便從Session中獲取屬性時可以從內存直接獲取提升性能:
private MapSession loadSession(String id, Map<Object, Object> entries) { MapSession loaded = new MapSession(id); for (Map.Entry<Object, Object> entry : entries.entrySet()) { String key = (String) entry.getKey(); if (CREATION_TIME_ATTR.equals(key)) { loaded.setCreationTime((Long) entry.getValue()); } else if (MAX_INACTIVE_ATTR.equals(key)) { loaded.setMaxInactiveIntervalInSeconds((Integer) entry.getValue()); } else if (LAST_ACCESSED_ATTR.equals(key)) { loaded.setLastAccessedTime((Long) entry.getValue()); } else if (key.startsWith(SESSION_ATTR_PREFIX)) { loaded.setAttribute(key.substring(SESSION_ATTR_PREFIX.length()), entry.getValue()); } } return loaded; }
至此,能夠看出spring-session中request.getSession(false)的過時實現原理。
delete方法:
public void delete(String sessionId) {
至此RedisOperationsSessionRepository的核心原理就介紹完畢。可是RedisOperationsSessionRepository中還包括關於Session事件的處理和清理Session的定時任務。這部份內容在後述的SessionEvent部分介紹。
5. HttpSessionStrategy
A strategy for mapping HTTP request and responses to a {@link Session}.
從javadoc中能夠看出,HttpSessionStrategy是創建Request/Response和Session之間的映射關係的策略。
Tips:策略模式
策略模式是一個傳神的神奇模式,是java的多態很是典型應用,是開閉原則、迪米特法則的具體體現。將同類型的一系列的算法封裝在不一樣的類中,經過使用接口注入不一樣類型的實現,以達到的高擴展的目的。通常是定義一個策略接口,按照不一樣的場景實現各自的策略。
該策略接口中定義一套策略行爲:
以下UML類圖:
這裏主要介紹CookieHttpSessionStrategy,這個也是默認的策略,能夠查看spring-session中類SpringHttpSessionConfiguration,在註冊SessionRepositoryFilter Bean時默認採用CookieHttpSessionStrategy:
@Bean public <S extends ExpiringSession> SessionRepositoryFilter<? extends ExpiringSession> springSessionRepositoryFilter( SessionRepository<S> sessionRepository) { SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<S>( sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); if (this.httpSessionStrategy instanceof MultiHttpSessionStrategy) { sessionRepositoryFilter.setHttpSessionStrategy( (MultiHttpSessionStrategy) this.httpSessionStrategy); } else { sessionRepositoryFilter.setHttpSessionStrategy(this.httpSessionStrategy); } return sessionRepositoryFilter; }
下面來分析CookieHttpSessionStrategy的原理。該策略使用Cookie來映射Request/Response至Session。即request/requset的head中cookie存儲SessionId,當請求至web服務器,能夠解析請求head中的cookie,而後獲取sessionId,根據sessionId獲取spring-session。當建立新的session或者session過時,將相應的sessionId寫入response的set-cookie或者從respose中移除sessionId。
getRequestedSessionId方法
public String getRequestedSessionId(HttpServletRequest request) {
接下來看下具體獲取SessionIds的具體過程:
public String getRequestedSessionId(HttpServletRequest request) {
再來看下獲取當前request對應的Session的別名方法getCurrentSessionAlias
public String getCurrentSessionAlias(HttpServletRequest request) {
spring-session爲了支持多session,才弄出多個session別名。當時通常應用場景都是一個session,都是默認的session別名0。
上述獲取sessionId和別名映射關係中,也是默認別名0。這裏返回別名0,因此返回當前請求對應的sessionId。
onNewSession方法
public void onNewSession(Session session, HttpServletRequest request, HttpServletResponse response) {
sessionIdsWritten主要是用來記錄已經寫入Cookie的SessionId,防止SessionId重複寫入Cookie中。
onInvalidateSession方法
public void onInvalidateSession(HttpServletRequest request, HttpServletResponse response) {
繼續看下具體的寫入writeCookieValue原理:
public void writeCookieValue(CookieValue cookieValue) {
至此,CookieHttpSessionStrategy介紹結束。
因爲篇幅過長,關於spring-session event和RedisOperationSessionRepository清理session而且產生過時事件的部分後續文章介紹。
總結
spring-session提供集羣環境下HttpSession的透明集成。spring-session的優點在於開箱即用,具備較強的設計模式。且支持多種持久化方式,其中RedisSession較爲成熟,與spring-data-redis整合,可謂威力無窮。
http://www.javashuo.com/article/p-uhnxejue-dn.html
上一篇文章中介紹了Spring-Session的核心原理,Filter,Session,Repository等等,傳送門:spring-session(一)揭祕。
這篇繼上一篇的原理逐漸深刻Spring-Session中的事件機制原理的探索。衆所周知,Servlet規範中有對HttpSession的事件的處理,如:HttpSessionEvent/HttpSessionIdListener/HttpSessionListener,能夠查看Package javax.servlet
在Spring-Session中也有相應的Session事件機制實現,包括Session建立/過時/刪除事件。
本文主要從如下方面探索Spring-Session中事件機制
Note:
這裏的事件觸發機制只介紹基於RedissSession的實現。基於內存Map實現的MapSession不支持Session事件機制。其餘的Session實現這裏也不作關注。
一.Session事件的抽象
先來看下Session事件抽象UML類圖,總體掌握事件之間的依賴關係。
Session Event最頂層是ApplicationEvent,即Spring上下文事件對象。由此能夠看出Spring-Session的事件機制是基於Spring上下文事件實現。
抽象的AbstractSessionEvent事件對象提供了獲取Session(這裏的是指Spring Session的對象)和SessionId。
基於事件的類型,分類爲:
- Session建立事件
- Session刪除事件
- Session過時事件
Tips:
Session銷燬事件只是刪除和過時事件的統一,並沒有實際含義。
事件對象只是對事件自己的抽象,描述事件的屬性,如:
- 獲取事件產生的源:getSource獲取事件產生源
- 獲取相應事件特性:getSession/getSessoinId獲取時間關聯的Session
下面再深刻探索以上的Session事件是如何觸發,從事件源到事件監聽器的鏈路分析事件流轉過程。
二.事件的觸發機制
閱讀本節前,讀者應該瞭解Redis的Pub/Sub和KeySpace Notification,若是還不是很瞭解,傳送門Redis Keyspace Notifications和Pub/Sub。
上節中也介紹Session Event事件基於Spring的ApplicationEvent實現。先簡單認識spring上下文事件機制:
- ApplicationEventPublisher實現用於發佈Spring上下文事件ApplicationEvent
- ApplicationListener實現用於監聽Spring上下文事件ApplicationEvent
- ApplicationEvent抽象上下文事件
那麼在Spring-Session中必然包含事件發佈者ApplicationEventPublisher發佈Session事件和ApplicationListener監聽Session事件。
能夠看出ApplicationEventPublisher發佈一個事件:
@FunctionalInterface public interface ApplicationEventPublisher {
ApplicationListener用於監聽相應的事件:
@FunctionalInterface public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
Tips:
這裏使用到了發佈/訂閱模式,事件監聽器能夠監聽感興趣的事件,發佈者能夠發佈各類事件。不過這是內部的發佈訂閱,即觀察者模式。
Session事件的流程實現以下:
上圖展現了Spring-Session事件流程圖,事件源來自於Redis鍵空間通知,在spring-data-redis項目中抽象MessageListener監聽Redis事件源,而後將其傳播至spring應用上下文發佈者,由發佈者發佈事件。在spring上下文中的監聽器Listener便可監聽到Session事件。
由於二者是Spring框架提供的對Spring的ApplicationEvent的支持。Session Event基於ApplicationEvent實現,必然也有其相應發佈者和監聽器的的實現。
Spring-Session中的RedisSession的SessionRepository是RedisOperationSessionRepository。全部關於RedisSession的管理操做都是由其實現,因此Session的產生源是RedisOperationSessionRepository。
在RedisOperationSessionRepository中持有ApplicationEventPublisher對象用於發佈Session事件。
private ApplicationEventPublisher eventPublisher = new ApplicationEventPublisher() { @Override public void publishEvent(ApplicationEvent event) { } @Override public void publishEvent(Object event) { } };
可是該ApplicationEventPublisher是空實現,實際實現是在應用啓動時由Spring-Session自動配置。在spring-session-data-redis模塊中RedisHttpSessionConfiguration中有關於建立RedisOperationSessionRepository Bean時將調用set方法將ApplicationEventPublisher配置。
@Configuration @EnableScheduling public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer { private ApplicationEventPublisher applicationEventPublisher; @Bean public RedisOperationsSessionRepository sessionRepository() { RedisTemplate<Object, Object> redisTemplate = createRedisTemplate(); RedisOperationsSessionRepository sessionRepository = new RedisOperationsSessionRepository( redisTemplate);
在進行自動配置時,將上下文中的ApplicationEventPublisher的注入,實際上即ApplicationContext對象。
Note:
考慮篇幅緣由,以上的RedisHttpSessionConfiguration至展現片斷。
對於ApplicationListener是由應用開發者自行實現,註冊成Bean便可。當有Session Event發佈時,便可監聽。
以上部分探索了Session事件的發佈者和監聽者,可是核心事件的觸發發佈則是由Redis的鍵空間通知機制觸發,當有Session建立/刪除/過時時,Redis鍵空間會通知Spring-Session應用。
RedisOperationsSessionRepository實現spring-data-redis中的MessageListener接口。
該監聽器即用來監聽redis發佈的消息。RedisOperationsSessionRepositorys實現了該Redis鍵空間消息通知監聽器接口,實現以下:
public class RedisOperationsSessionRepository implements FindByIndexNameSessionRepository<RedisOperationsSessionRepository.RedisSession>, MessageListener { @Override @SuppressWarnings("unchecked") public void onMessage(Message message, byte[] pattern) {
下續再深刻每種事件產生的前世此生。
1.Session建立事件的觸發
- 由RedisOperationSessionRepository向Redis指定通道${namespace}:event:created:${sessionId}發佈一個message
- MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道${namespace}:event:created:${sessionId}的消息
- 將其傳播至ApplicationEventPublisher
- ApplicationEventPublisher發佈SessionCreateEvent
- ApplicationListener監聽SessionCreateEvent,執行相應邏輯
RedisOperationSessionRepository中保存一個Session時,判斷Session是否新建立。
若是新建立,則向
@Override public void save(RedisSession session) { session.saveDelta();
該save方法的調用是由HttpServletResponse提交時——即返回客戶端響應調用,上篇文章已經詳解,這裏再也不贅述。關於RedisOperationSessionRepository實現MessageListener上述已經介紹,這裏一樣再也不贅述。
Note:
這裏有點繞。我的認爲RedisOperationSessionRepository發佈建立而後再自己監聽,主要是考慮分佈式或者集羣環境中SessionCreateEvent事件的處理。
2.Session刪除事件的觸發
Tips:
刪除事件中使用到了Redis KeySpace Notification,建議先了解該技術。
- 由RedisOperationSessionRepository刪除Redis鍵空間中的指定Session的過時鍵,Redis鍵空間會向**__keyevent@*:del**的channel發佈刪除事件消息
- MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道**__keyevent@*:del**的消息
- 將其傳播至ApplicationEventPublisher
- ApplicationEventPublisher發佈SessionDeleteEvent
- ApplicationListener監聽SessionDeleteEvent,執行相應邏輯
當調用HttpSession的invalidate方法讓Session失效時,即會調用RedisOperationSessionRepository的deleteById方法刪除Session的過時鍵。
上篇中介紹了包裝Spring Session爲HttpSession,這裏再也不贅述。這裏重點分析deleteById內容:
@Override public void deleteById(String sessionId) {
後續流程同SessionCreateEvent流程。
3.Session失效事件的觸發
Session的過時事件流程比較特殊,由於Redis的鍵空間通知的特殊性,Redis鍵空間通知不能保證過時鍵的通知的及時性。
- RedisOperationsSessionRepository中有個定時任務方法每整分運行訪問整分Session過時鍵集合中的過時sessionId,如:spring:session:expirations:1439245080000。觸發Redis鍵空間會向**__keyevent@*:expired**的channel發佈過時事件消息
- MessageListener的實現RedisOperationSessionRepository監聽到Redis指定通道**__keyevent@*:expired**的消息
- 將其傳播至ApplicationEventPublisher
- ApplicationEventPublisher發佈SessionDeleteEvent
- ApplicationListener監聽SessionDeleteEvent,執行相應邏輯
@Scheduled(cron = "0 * * * * *") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); }
定時任務每整分運行,執行cleanExpiredSessions方法。expirationPolicy是RedisSessionExpirationPolicy實例,是RedisSession過時策略。
public void cleanExpiredSessions() {
將時間戳滾動至整分
static long roundDownMinute(long timeInMs) {
Calendar date = Calendar.getInstance(); date.setTimeInMillis(timeInMs); // 清理時間錯的秒位和毫秒位 date.clear(Calendar.SECOND); date.clear(Calendar.MILLISECOND); return date.getTimeInMillis(); }
獲取過時Session的集合
String getExpirationKey(long expires) { return this.redisSession.getExpirationsKey(expires); }
調用Redis的Exists命令,訪問過時Session鍵,觸發Redis鍵空間消息
總結
至此Spring-Session的Session事件通知模塊就已經很清晰:
- Redis鍵空間Session事件源:Session建立通道/Session刪除通道/Session過時通道
- Spring-Session中的RedisOperationsSessionRepository消息監聽器監聽Redis的事件類型
- RedisOperationsSessionRepository負責將其傳播至ApplicationEventPublisher
- ApplicationEventPublisher將其包裝成ApplicationEvent類型的Session Event發佈
- ApplicationListener監聽Session Event,處理相應邏輯
http://www.javashuo.com/article/p-aqthueky-cc.html
沒有設置這個,好奇怪:
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 604800)
前兩篇介紹了spring-session的原理,這篇在理論的基礎上再實戰。
spring-boot整合spring-session的自動配置可謂是開箱即用,極其簡潔和方便。這篇文章即介紹spring-boot整合spring-session,這裏只介紹基於RedisSession的實戰。
原理篇是基於spring-session v1.2.2版本,考慮到RedisSession模塊與spring-session v2.0.6版本的差別很小,且可以與spring-boot v2.0.0兼容,因此實戰篇是基於spring-boot v2.0.0基礎上配置spring-session。
源碼請戮session-example
實戰
搭建spring-boot工程這裏飄過,傳送門:https://start.spring.io/
配置spring-session
引入spring-session的pom配置,因爲spring-boot包含spring-session的starter模塊,因此pom中依賴:
<dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency>
編寫spring boot啓動類SessionExampleApplication
配置application.yml
spring: session: redis: flush-mode: on_save namespace: session.example cleanup-cron: 0 * * * * * store-type: redis timeout: 1800 redis: host: localhost port: 6379 jedis: pool: max-active: 100 max-wait: 10 max-idle: 10 min-idle: 10 database: 0
除了配置spring.session.timeout外,還要配置:
和
配置cookieMaxAge後,前端瀏覽器都會有Max-Age和Expires數據
編寫controller
編寫登陸控制器,登陸時建立session,並將當前登陸用戶存儲sesion中。登出時,使session失效。
編寫查詢控制器,在登陸建立session後,使用將sessionId置於cookie中訪問。若是沒有session將返回錯誤。
編寫Session刪除事件監聽器
Session刪除事件監聽器用於監聽登出時使session失效的事件源。
驗證測試
編寫spring-boot測試類,測試controller,驗證spring-session是否生效。
測試類執行結果:
create session, sessionId is:429cb0d3-698a-475a-b3f1-09422acf2e9c current's user is:429cb0d3-698a-475a-b3f1-09422acf2e9cin session invalid session's user:UserVo{userName='admin', userPassword='admin@123'
登陸時建立Session,存儲當前登陸用戶。而後在以登陸響應返回的SessionId查詢用戶。最後再登出使Session過時。
spring-boot整合spring-session自動配置原理
前兩篇文章介紹spring-session原理時,總結spring-session的核心模塊。這節中探索spring-boot中自動配置如何初始化spring-session的各個核心模塊。
spring-boot-autoconfigure模塊中包含了spinrg-session的自動配置。包org.springframework.boot.autoconfigure.session中包含了spring-session的全部自動配置項。
其中RedisSession的核心配置項是RedisHttpSessionConfiguration類。
@Configuration @ConditionalOnClass({ RedisTemplate.class, RedisOperationsSessionRepository.class }) @ConditionalOnMissingBean(SessionRepository.class) @ConditionalOnBean(RedisConnectionFactory.class) @Conditional(ServletSessionCondition.class) @EnableConfigurationProperties(RedisSessionProperties.class) class RedisSessionConfiguration { @Configuration public static class SpringBootRedisHttpSessionConfiguration extends RedisHttpSessionConfiguration {
RedisSessionConfiguration配置類中嵌套SpringBootRedisHttpSessionConfiguration繼承了RedisHttpSessionConfiguration配置類。首先看下該配置類持有的成員。
@Configuration @EnableScheduling public class RedisHttpSessionConfiguration extends SpringHttpSessionConfiguration implements BeanClassLoaderAware, EmbeddedValueResolverAware, ImportAware, SchedulingConfigurer {
該配置類中初始化了RedisSession的最爲核心模塊之一RedisOperationsSessionRepository。
@Bean public RedisOperationsSessionRepository sessionRepository() {
同時也初始化了Session事件監聽器MessageListener模塊
@Bean public RedisMessageListenerContainer redisMessageListenerContainer() {
上篇文章中介紹到的spring-session event事件原理,spring-session在啓動時監聽Redis的channel,使用Redis的鍵空間通知處理Session的刪除和過時事件和使用Pub/Sub模式處理Session建立事件。
關於RedisSession的存儲管理部分已經初始化,可是spring-session的另外一個基礎設施模塊SessionRepositoryFilter是在RedisHttpSessionConfiguration父類SpringHttpSessionConfiguration中初始化。
@Bean public <S extends Session> SessionRepositoryFilter<? extends Session> springSessionRepositoryFilter( SessionRepository<S> sessionRepository) { SessionRepositoryFilter<S> sessionRepositoryFilter = new SessionRepositoryFilter<>( sessionRepository); sessionRepositoryFilter.setServletContext(this.servletContext); sessionRepositoryFilter.setHttpSessionIdResolver(this.httpSessionIdResolver); return sessionRepositoryFilter; }
spring-boot整合spring-session配置的層次:
RedisSessionConfiguration |_ _ SpringBootRedisHttpSessionConfiguration |_ _ RedisHttpSessionConfiguration |_ _ SpringHttpSessionConfiguration
回顧思考spring-boot自動配置spring-session,很是合理。
- SpringHttpSessionConfiguration是spring-session自己的配置類,與spring-boot無關,畢竟spring-session也能夠整合單純的spring項目,只須要使用該spring-session的配置類便可。
- RedisHttpSessionConfiguration用於配置spring-session的Redission,畢竟spring-session還支持其餘的各類session:Map/JDBC/MogonDB等,將其從SpringHttpSessionConfiguration隔離開來,遵循開閉原則和接口隔離原則。可是其必須依賴基礎的SpringHttpSessionConfiguration,因此使用了繼承。RedisHttpSessionConfiguration是spring-session和spring-data-redis整合配置,須要依賴spring-data-redis。
- SpringBootRedisHttpSessionConfiguration纔是spring-boot中關鍵配置
- RedisSessionConfiguration主要用於處理自定義配置,將application.yml或者application.properties的配置載入。
Tips:
配置類也有至關強的設計模式。遵循開閉原則:對修改關閉,對擴展開放。遵循接口隔離原則:變化的就要單獨分離,使用不一樣的接口隔離。SpringHttpSessionConfiguration和RedisHttpSessionConfiguration的設計深深體現這兩大原則。
參考
Spring Session
http://www.javashuo.com/article/p-zxxoxqfw-cp.html