对 Session 的深入探讨
簡介
session,會話,其實是一個容易讓人誤解的詞。它總跟web系統的會話掛鉤,利用session,javaweb項目實現了登錄狀態的控制。坊間流傳,關閉瀏覽器,就是關閉了web系統的會話。其實瀏覽器對于會話有自己的定義,而web系統對于會話也有自己的定義。在tomcat中,session通常是指實現了HttpSession接口的實現類。并且不存在關閉瀏覽器就會關閉tomcat的HttpSession這種狀況。
session本身并不難,如果只是做登錄校驗之類的功能,并不需要深入了解,但難的是session和cookie的結合使用,在不同情況下瀏覽器對cookie的控制行為所涉及到的諸多細節,我搜查了很多資料,查看過tomcat源碼,亦是沒有找到全面的概述。當然我并未看過、也不知道去哪里看比較全面的關于瀏覽器對cookie的控制資料,如果有知道的大神,還望留言鏈接。本文題目,之所以說是探討,而不是了解或者介紹,因為我自己也卡在了某個點上,由于時間關系,我不能花太多時間去研究,但又不忍心就此放棄,所以先記錄下來,日后有機會再研究,這期間如有大神指點,也許能讓我茅塞頓開。
session本質
我用的是javaweb項目,因此這里的session特指HttpSession。先來看下tomcat源碼中對session的設計,在org.apache.catalina.session包下,有如下設計:
平時所用到的HttpSession的實現類就是這個standardSession。但是所獲取的HttpSession實例確是外觀類StandardSessionFacade,其屏蔽了許多方法,但也增強了安全性。HttpSession提供了一些方法,來控制session或者獲取session的狀態,如獲取session的id,獲取session的創建時間,設置session的attribute,使session失效等。值得一提的是session的attribute其實是一個線程安全的hashMap:
????/***?The?collection?of?user?data?attributes?associated?with?this?Session.*/protected?ConcurrentMap<String,?Object>?attributes?=?new?ConcurrentHashMap<>();但是,創建session、根據id獲取session的方法并不在這里,而是在一個管理器中,其設計如下:
ManagerBase是實現了Manager接口的抽象類,實現了管理session的功能。其實現子類PersistentManagerBase拓展了將session持久化的功能。但是這里不需要講到其子類。看ManagerBase中的一段代碼:
????/***?The?set?of?currently?active?Sessions?for?this?Manager,?keyed?by*?session?identifier.*/protected?Map<String,?Session>?sessions?=?new?ConcurrentHashMap<>();由此可知,所謂的session,其實就是一個用線程安全的hashMap存儲起來的實現了Session接口的standardSession對象,在hashmap中以其id為key,自身為value。
再看獲取session的方法,一目了然:
????@Overridepublic?Session?findSession(String?id)?throws?IOException?{if?(id?==?null)?{return?null;}return?sessions.get(id);}最重要的是看其createSession方法:
?@Overridepublic?Session?createSession(String?sessionId)?{if?((maxActiveSessions?>=?0)?&&(getActiveSessions()?>=?maxActiveSessions))?{rejectedSessions++;throw?new?TooManyActiveSessionsException(sm.getString("managerBase.createSession.ise"),maxActiveSessions);}//?Recycle?or?create?a?Session?instanceSession?session?=?createEmptySession();//?Initialize?the?properties?of?the?new?session?and?return?itsession.setNew(true);session.setValid(true);session.setCreationTime(System.currentTimeMillis());session.setMaxInactiveInterval(getContext().getSessionTimeout()?*?60);String?id?=?sessionId;if?(id?==?null)?{id?=?generateSessionId();}session.setId(id);sessionCounter++;SessionTiming?timing?=?new?SessionTiming(session.getCreationTime(),?0);synchronized?(sessionCreationTiming)?{sessionCreationTiming.add(timing);sessionCreationTiming.poll();}return?session;}這個方法是在什么時候調用的呢?當瀏覽器訪問系統時,request會解析請求中攜帶的jssesionid,用它去找到存在于應用中的session,但是如果沒有找到,那么就會調用session的創建方法,并且生成一個新的jssessionid,返回session。
總而言之,session是存在于線程安全的map中的值,可以通過id找到,也可以使用invalidate方法銷毀,但絕不會是瀏覽器關閉,就能對它進行銷毀的。
cookie簡介
提到session,那么cookie是不得不說的。至于cookie是什么,我就不多說了,大家都懂。直接看其內容吧:
這是一次http請求中(http://localhost:8080/test1),包含的請求和響應信息,是對一個系統的初次訪問,用的是谷歌瀏覽器。
請求頭中,包含的Cookie信息,并沒有上文提到的jsessionid, 那是因為這是對系統的初次訪問,系統還沒生成session。但是訪問之后,系統就會生成一個session,而且,會在響應流中設置響應頭Set-Cookie,其值為JESSIONID=xxx。這樣瀏覽器對localhost:8080和cookie的聯系就有了記憶,瀏覽器會將其存儲起來,可在調試工具中看到:
那么再次訪問http://localhost:8080/test1, 瀏覽器會主動在請求頭添加包括jsession的cookie信息
系統根據這個jsessionid找到session,也就不會在響應頭中添加Set-Cookie信息。
這里說一下cookie中的兩個重要屬性:
domain表示的是cookie所在的域,默認為請求的地址,如網址為www.test.com/test/test.aspx,那么domain默認為www.test.com。而跨域訪問,如域A為t1.test.com,域B為t2.test.com,那么在域A生產一個令域A和域B都能訪問的cookie就要將該cookie的domain設置為.test.com;如果要在域A生產一個令域A不能訪問而域B能訪問的cookie就要將該cookie的domain設置為t2.test.com。
path表示cookie所在的目錄,默認為/,就是根目錄。在同一個服務器上有目錄如下:/test/,/test/cd/,/test/dd/,現設一個cookie1的path為/test/,cookie2的path為/test/cd/,那么test下的所有頁面都可以訪問到cookie1,而/test/和/test/dd/的子頁面不能訪問cookie2。這是因為cookie能讓其path路徑下的頁面訪問。
疑點
下面,就該說下我的疑點了。
情況1:
但是當我在8081的一個方法中,重定向到8080的一個路徑時,發現了奇怪的現象。
8081系統的方法如下:
???@GetMapping("/test")public?void?get1(HttpServletRequest?request,?HttpServletResponse?response)?throws?IOException?{HttpSession?session?=?request.getSession();String?id?=?session.getId();System.out.println(id);response.sendRedirect("http://localhost:8080/test1");}8080系統的被重定向路徑如下:
????@GetMapping("/test1")public?void?get11(HttpServletRequest?request,?HttpServletResponse?response)?throws?IOException?{HttpSession?session?=?request.getSession();String?id?=?session.getId();System.out.println(id);}1、初次訪問localhost:8081/test 得到兩次請求的信息,一次是重定向的,一次是8080的
這說明對8081系統的初次訪問,是沒有發送jsessionid信息的,而8081系統生成了一個id為CAAB6AED34716A0394705BDE8CAC0042的session并設置到了響應頭,再次訪問8081時理應會帶上這么一個id。
2、
這個對8080系統的請求中帶有jsessionid為CAAB6AED34716A0394705BDE8CAC0042的cookie信息,要知道,我們對8080的訪問也是初次的,那么為什么會帶上jsessionid呢?而且這個jsessionid明顯是在8081系統中生成并設置到響應頭的的jsessionid。這個現象我用谷歌和edge瀏覽器分別嘗試過,都是這樣。那么是不是說明,瀏覽器把這個重定向到localhost:8080的請求當成是同域的請求了 。
暫且放下這個疑惑,繼續往下驗證。由于這個請求是對8080的系統的訪問,由于是初次訪問,系統根本沒有id為CAAB6AED34716A0394705BDE8CAC0042的session,因此只好生成一個新的session,在響應頭中增加Set-Cookie。
3、再次訪問localhost:8081/test,這時根據上文說的,“再次訪問8081時理應會帶上這么一個id”,也就是在cookie中帶上JSESSION=CAAB6AED34716A0394705BDE8CAC0042, 但是,我發現它帶的卻是在系統8080中生成的BA0D2C939ADEC087C0A5F0C9B3354891 !!!
這就導致了8081找不到session又再次生成了一個新的session,循環往復,每次對8081的訪問都會產生新的session。而這情況,我覺得很明顯,是瀏覽器把對8081的訪問當成是于8080同源的了。
基于此推論,我模擬了另一種實驗情況,去掉重定向的功能:
情況2:
在本地開兩個web服務,端口分別是8080,8081。
localhost:8081/test
????@GetMapping("/test")public?void?get1(HttpServletRequest?request,?HttpServletResponse?response)?throws?IOException?{HttpSession?session?=?request.getSession();String?id?=?session.getId();System.out.println(id);}localhost:8080/test1
????@GetMapping("/test1")public?void?get11(HttpServletRequest?request,?HttpServletResponse?response)?throws?IOException?{HttpSession?session?=?request.getSession();String?id?=?session.getId();System.out.println(id);}1、第一次訪問8081/test
沒有cookie,服務器設置set-cookie,正常。
2、第二次訪問8081/test
cookie與上次的set-cookie一致,正常。
3、第一次訪問8080/test1
瀏覽器把8081/test的cookie發過去了。8080的服務器找不到這個jsessionid,又重新設置了jsessionid,等到再次訪問8081/test時,大家也能猜到會發生什么了吧。
推論
至此,我斗膽推論,瀏覽器會對同一ip不同端口的服務訪問認定是可以進行cookie共享的,兩個cookie的domain是一致的。而這種cookie的截圖也一定程度上印證了我的想法:
cookie的domain似乎只認定域名,無關端口。
但是根據瀏覽器的同源策略,同域名不同端口的訪問也應該是跨域的啊。除非瀏覽器的域跟cookie的domain在概念上是有區別的,對于這點,我沒找到確切的官方資料,但網上大神是這么說的——
解決方案
基于上面的未查閱官方資料而做出的不嚴謹的推論,我想,只要完全避免同域的情況就可以避開這個問題。于是我把8081和8080系統分別部署在兩個機器上。由于不同ip,這樣無論如何,兩個cookie都不會是同domain的了。果然,結果是沒有問題的。
不足
雖然這個解決方案避開了同域的問題,但是沒有徹底解決,畢竟同域的系統相互之間的訪問也是有必要的,為此希望能獲得更多的建議或者資料,補充這方面知識的不足,讓我徹底解決這個問題。
總結
以上是生活随笔為你收集整理的对 Session 的深入探讨的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: redis缓存策略小结
- 下一篇: 迎接 2019:软件开发新趋势预测