關于 Session 會話深入探讨簡介session本質cookie簡介疑點推論解決方案不足

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的方法,一目了然:
@Override
public Session findSession(String id) throws IOException {
if (id == null) {
return null;
}
return sessions.get(id);
}
最重要的是看其createSession方法:
@Override
public Session createSession(String sessionId) {
if ((maxActiveSessions >= 0) &&
(getActiveSessions() >= maxActiveSessions)) {
rejectedSessions++;
throw new TooManyActiveSessionsException(
sm.getString("managerBase.createSession.ise"),
maxActiveSessions);
}
// Recycle or create a Session instance
Session session = createEmptySession();
// Initialize the properties of the new session and return it
session.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的了。果然,結果是沒有問題的。
不足
雖然這個解決方案避開了同域的問題,但是沒有徹底解決,畢竟同域的系統互相之間的通路也是有必要的,為此希望能獲得更多的建議或者資料,補充這方面知識的不足,讓我徹底解決這個問題。
作者:千裡明月
my.oschina.net/mingyuelab/blog/2986928
往期精選 點選标題可跳轉
從零開始,徒手撸一個簡單的 RPC 架構,輕松搞定!
Spring Boot 2.x 推薦緩存架構 Caffeine 高性能設計剖析
在 Java 項目中,如何使用 Error 日志更加友善排查問題,這才是正确姿勢,非常實用!
19 張思維導圖帶你梳理 Spring Cloud 體系中的重要知識點!
程式員經常浏覽的 60 個技術網站彙總,還不趕快收藏!
還在手動部署 Spring Boot 項目?使用 Docker 真香!
【Stackoverflow 問題】為什麼資深的程式員都不用 “ ! = null " 做判空?
你還在使用 BeanUtils 來做對象轉換嗎?快試試 MapStruct 吧!
Spring boot 內建阿裡開源 Sentinel 限流神器,輕松搞定接口限流!
關于分布式鎖 Redis 與 Zookeeper 的原理,它們如何實作分布式鎖?
IntelliJ IDEA 2020.2.3 版本永久破解激活詳細教程,親測有效!
點個贊,就知道你“在看”!