原文鏈接: https://blog.by24.cn/archives/about-session.html
Web 開發中,Session 是經常用到的概念,但是在日常交流中,似乎又經常引起誤解,
在我看來,引發誤解的原因主要有兩個:
- 大量使用簡稱,導致混淆了 資料 與 索引
- 不同 語言/框架 對 Session 做了不同形式的封裝,導致特征不同
下面,我就嘗試著捋一下整個問題,看看能不能盡量消除這些誤會,
注:其它通訊領域也有使用 Session 這個詞匯,本文僅探討 Web 開發中的 Session 使用,
TL;DR
本文確實冗長無比,此處列出簡要觀點方便『太長不看』(Too Long; Didn't Read.)的讀者,
- 除了常見的 Server Side Session,應當注意還有 Client Side Session 的存在
- Session 是虛擬概念,應當注意區分 Session-ID 和 Session-Data
- Session 的具體實作,與 Cookies 沒有系結關系
- JWT 和 Session 壓根不在一個維度,不應該放在一起討論
Session 是什么
名詞約定
由于大家使用的『簡稱』含義往往差別很大,在此我們先約定一些稱呼,
這些稱呼可能和你慣用的不太一樣,但統一定義是高效討論的重要基礎,還請不要抗拒,
Session
Session 經常被翻譯為 『會話』,其實還是很貼切的,它代表了『一次』相互溝通,這次會話中,可能包括了多次通信,
需要注意的是:此處 Session 是一個整體性的虛擬概念,不代指任何物體或資料,
Session-Data
看到 Data 后綴就知道,這個詞代表了一次 Session 中,暫存或使用 的一些資料,
具體是什么資料取決于業務實作,例如可能存盤了一些用戶的 ID 資訊或短效配置等,
需要注意的是,此處指的是資料的『物體』,而不是『索引』,
Session-ID
上面說到的資料物體 Session-Data ,未必會直接放在程式內部,而是可能存盤在某一個具體的位置(后文會詳細討論),
在大部分 Server Side Session 實作中(即 Session-Data 存盤在服務端時),為了方便取用,就需要記錄一個『索引』,用來找到具體的 Session-Data ,我們將這個『索引』稱為 Session-ID ,
需要注意的是:在某些實作中,可能并沒有 Session-ID 的概念(例如 Client Side Session),
存盤的差異
正如我前面所強調的,在不同的實作中,Session-Data 可能存盤在不同的位置:可能在客戶端,也可能在服務端,
Server Side Session
在大部分實作中,并不會直接傳輸 Session-Data ,而是傳輸它的索引:Session-ID ,服務端程式收到 Session-ID 后,從存盤著 Session-Data 的地方取出具體的 Session-Data 來使用,
在這種模式下,更容易被我們接觸到的,其實是存盤在 Cookies 里的 Session-ID ,而許多人存在的一個誤區,便是將 Session-ID 直接等同于 Session 本身,無視了 Session-Data 的存在,這其實是不正確的,

上圖表示了這種模式的大致運行方式,綠色的 Session 是一個老用戶會話, Alice 在訪問時發送了自己持有的 Session-ID AA01 來標記當前會話,服務器程式根據相應的 Session-id 查詢出了相應的 Session Data ,并在程式中應用,
對于新用戶,因為自己沒有存盤 Session-id,只好不發送,服務器為了標記本次會話,生成了新的 Session-ID NN02 并回傳給客戶端,這樣就開啟了一個新的會話,圖中用紅色 Session 表示,
Client Side Session
除了上面提到的常見模式,其實還存在一種實作被許多人忽視,服務端并不存盤 Session-Data,而是直接將 Session-Data 存盤在客戶端(瀏覽器)的 Cookies 里,
在這種情況下,你甚至找不到 Session-ID 的存在,許多人也會因此而搞不清 Session 究竟存盤在哪里,

上圖表示了這種模式的大致運行方式,與上面那種方式不同的地方在于,客戶端直接告訴服務端,我自己是 Alice ,我的基本資訊是 xxx ,于是服務端使用這些資料回傳了相應的頁面,
對于新用戶,本地一丁點兒資訊都沒有,于是服務端回傳了一個空的 Session-Data 用來初始化,在后續的通信中,這個 Session-Data 會被繼續填充上有意義的資料,
很顯然,這里存在著巨大的安全問題:我直接修改存盤在本地的 Session-Data,聲稱我自己是管理員怎么辦?
為此,Client side session 需要加入加密機制或簽名機制來進行校驗,以確保所有的 Session-Data 都是由服務端生成的,具體會在下文討論,
和 Cookies 的關系
提到 Session 就不能不提 Cookies,畢竟很多人會條件反射的把這二者弄混(苦笑臉),
Cookies 自身就有非常多的玩兒法,拋開那些權限相關的內容,此處我們先只它最基礎的幾個特性:
- 在客戶端(瀏覽器)記憶體儲一些資料(而不是在服務端)
- 這些資訊,可以由服務端設定,也可以由客戶端設定
- 當用戶發起 HTTP 請求時,匹配的 Cookies 會嵌入請求頭部(Header)一起發送
正是這些特征,讓 Cookies 成為存盤 Session-Data / Session-ID 的絕佳場所,在幾乎所有的常見實作中,均默認使用 Cookies 來存盤 Session 相關資訊,
但是需要注意,Session-Data / Session-ID 選擇 Cookie 作為容器只是因為『方便』,但這并不是必然情況:
我們完全可以實作一個 Web 框架,它使用 LocalStorage 來存盤 Session-Data / Session-ID ,在發起 XHR 請求時,主動將其加進 Header 內(甚至 URL 內),服務端則從相應位置讀取資訊完成業務邏輯,在這套框架體系內這是完全沒有問題的,
實際上,許多框架都支持 Session with out Cookies 的實作,雖然存在各種各樣的小問題或限制,但是確實可用,
因此,需要再次強調:對于 Session 機制來說,Session-Data / Session-ID 才是本體,Cookies 只是一個存盤容器,這二者沒有捆綁關系,
Session 機制的實作
對于 Session 機制,之所以總是產生種種誤解, 和各大 語言 / 框架 的實作有著很大的關聯,由于大部分人都更熟悉自己常用的框架,導致很容易被當前框架的思路帶進去,
那么接下來,就看一下各種常用 語言 / 框架 在 Session 機制上的實作與差異,
注:以下只是各個 語言 / 框架 默認支持的實作,實際中我們完全可以自定義任何形式的實作,
PHP
原生 PHP
PHP 不愧是為 Web 而生的語言,在語言層面就提供了 Session 機制的支持,按默認配置就可以使用 $_SESSION 這個超全域變數,
原生 PHP 默認使用的 Session-Data 存盤方式為 Server Side Session ,使用 Session-ID 進行索引,Session-ID 默認存盤在 Cookies 的 PHPSESSID 欄位,
Session-Data 默認以檔案形式存盤在 session_save_path 所配置的路徑中, PHPSESSID欄位的內容就是檔案名,檔案內容就是序列化后的 Session-Data 資料,
通過配置 session.save_handler 欄位,我們可以將存盤方式切換為 redis 或者 memcache ,
以 redis 為例,Session-Data 會被存盤在PHPREDIS_SESSION:PHPSESSID 中,PHPREDIS_SESSION:為默認的統一前綴,
除此之外,還有一些其它的配置可以參考官方檔案,原生 PHP 也支持其它的相關擴展,不再一一贅述,
Laravel
Laravel 果然也很強,不但默認提供了常規的 Server Side Session ,也支持配置為 Client Side Session 形式,
通過配置 config/session.php 的 driver 欄位,可以切換 file 、cookie 、database 、memcached 、redis 、array 這 6 種不同的存盤方式,
這里面, file 、memcached 、redis 都是比較常見的存盤方式,就不再贅述,
database 可以將 Session-Data 存盤在你定義的資料庫內,但是你需要先建好一張表,里面配置好要存盤的欄位,
array 是一種比較奇怪的方式,只是用在開發階段,它并沒有任何持久化功能,隨著 PHP 腳本的運行結束而消失,
cookie 則是比較少見的** Client Side Session**了,Laravel 使用 EncryptedStore.php 對 Session-Data 進行 『加密』后存盤于客戶端瀏覽器的 Cookies 中,
這里需要注意,Laravel 在實作 **Client Side Session **的時候使用的是 『加密』方式來保障資訊安全,
CodeIgniter
接下來看看老牌框架 CI 怎么樣,翻看 配置檔案 可以看到,CI4 默認支持 file 、database 、memcached 、redis 這幾種 Server Side Session,看起來似乎平平無奇,都是常見套路,
但是檔案上有一行備注:
In previous CodeIgniter versions, a different, “cookie driver” was the only option and we have received negative feedback on not providing that option. While we do listen to feedback from the community, we want to warn you that it was dropped because it is unsafe and we advise you NOT to try to replicate it via a custom driver.
這就很有意思了,看起來 CI 框架以前也支持Client Side Session 嘛?翻出了 CI2 的檔案 ,果然里面默認使用 Cookies 存盤 Session-Data ,也提供了加密的選項,
那為什么 CI2 當時會因為這個被錘呢?我找到一篇相關的文章:(IN)secure session data in CodeIgniter - Websec ,文章中的觀點來看:
- 雖然提供了對 Cookies 進行加密的選項,但默認配置是關閉的,這就導致了 Session-Data 會以明文展示,可能會泄漏資訊,
- 雖然 CI2 在加密的基礎上,還對明文 Session-Data 進行了簽名,但是在用戶配置了弱密鑰的情況下,簽名密鑰可能會被暴力破解,導致 Session-Data 被篡改,
其實這兩個問題都更應該歸咎于用戶的配置不當,但是畢竟和框架的默認配置有關,被錘也就在情理之中了,
可能也是因為這件事,才導致了 CI 現在只提供 Server Side Session 實作吧,
其它 PHP 框架
翻看了一下 Symfony、Yii 、CakePHP、ThinkPHP 等熱門框架關于 Session 的檔案,都使用了比較常規的 Server Side Session 方式,也都支持將 Session-Data 存盤在檔案、資料庫、記憶體資料庫等,在此就不一一介紹了,有需要的可以點鏈接查看相應檔案,
Java
Tomcat
Tomcat 也是 Server Side Session 一派,默認在 Cookies 中使用 JSESSIONID 欄位存盤 Session-ID,Session-Data 默認存盤在記憶體中 ,翻閱 org.apache.catalina.session.ManagerBase 的原始碼,可以看到它使用了一個ConcurrentHashMap 結構來存盤所有的 Session-Data 實體,
Tomcat 默認使用 StandardManager 來管理 Session-Data,當容器退出時,在退出時將所有的資料進行持久化;你也可以選擇使用 PersistentManager 來管理,它可以更加靈活的執行持久化(但是 Session-Data 還是會存盤在你的記憶體里),
Jetty
Jetty 也比較中規中矩,參照檔案來看,支持將 Session-Data 配置為存盤在 記憶體、檔案、JDBC 等位置,
比較不同的是,Jetty 支持為 Session-Data 配置 L1、 L2 兩級快取,在對性能有需求的場景下還是挺友好,
Spring Session
果然是家大業大的框架,Spring Session 自己就是個完整的組件了,毫無意外的支持把 Session-Data 儲存在 redis 、mongodb ,jdbc 等地方,方便擴展,
不知如此,它還有一些特有的特性:
- 支持將 Session-ID 配置在 Request Header 的
X-Auth-Token欄位,更好的兼容 RESTful API - 可以在 WebSockt 中使用 ,且可以和 HTTP 側的 Session 機制相互配合使用
- 可以和 Spring Security 配合,實作更多權限管控相關功能
在舊一些的版本中,Spring Session 還支持 在一個瀏覽器會話中,維護多個 Session ,不過在新版本中,這個特性被洗掉了,也許以后才會加回來,
這個巨無霸框架其實還支持其它一大堆特性,這里就不太多介紹了,
Python
Django
作為一個功能完善的重量級框架,Django 默認直接將 Session-Data 存盤在了資料庫,當然,你也可以配置為其它的存盤方式,不再贅述,
這些存盤方式中,Client Side Session 的代表 Cookies 再次出現,Session-Data 在序列化并『簽名』后,存盤在客戶端 Cookies 中,
這里需要注意,此處是『簽名』而不是『加密』,也就是說,客戶端雖然不能篡改 Session-Data,但是可以自己反序列化后查看內容,
另外,Django 的 Session-data 還支持使用 Pickle 進行序列化,對于某些場景會非常有用(同時也帶來了一些潛在風險),
Flask
翻下檔案,專注于輕量的框架 Flask ,毫無疑問的選擇了實作起來最簡單的方案:直接用Client Side Session 就行了,
把 Session-Data 序列化并『簽名』后,存盤在客戶端 Cookies 中就搞掂啦,這里同樣需要注意,是『簽名』而不是『加密』,
不過這畢竟是個 Python 框架,人家包多啊,使用 Flask-Session 就可以獲得更多功能,支持將 Session-Data 存盤在Cookies、檔案、Redis、資料庫等各種地方,
Tornado
這個已經接近涼涼的框架表示:不好意思,我們沒有現成的 Session 模塊,想要用?那你自己寫去~
Why doesn't Tornado have session - Stack Exchange
Node.js
Express
Express 自身是最小化實作,Session 相關的功能由各種中間件來實作,此處主要提兩個中間件,
express-session 是典型的 Server Side Session 實作,依靠強大的包數量支持很多種存盤后端,
比較有意思的是,雖然是 Server Side Session 實作,express-session 依然不放心,給 Cookies 里的 Session-ID 加了一層『簽名』做校驗,可以說非常心細了,
cookie-session 相對就比較普通,就是一個常規的 Client Side Session 實作, Session-Data 在序列化并『簽名』后,存盤在客戶端的 Cookies 內,
Koa.js
包多就是好啊,koa 也有一大堆 Session 相關的中間件,在此就不一一介紹了,
Go
Gin
翻了下 Gin 比較常用的中間件是 gin-contrib/sessions ,同時支持 Server / Client Side Session ,也支持多種儲存后端,
這個中間件默認支持在一個瀏覽器會話中,維護多個 Session ,對特定的需求會方便一些,
Echo
Echo 的檔案中,默認使用了 gorilla/sessions 這個中間件,也是同時支持 Server / Client Side Session ,可以擴展支持其它存盤后端,
這個中間件也默認支持在一個瀏覽器會話中,維護多個 Session多個 Session ,對特定的需求會方便一些,
Beego
按照 Beego 的英文檔案來看,默認只支持 Server Side Session,但是奇怪的是中文檔案上卻又寫著支持的后端引擎包括 Cookie ,可能是檔案寫錯了,
Ruby
Rails
Rails 默認使用 Client Side Session 實作,Session-Data 『加密』并『簽名』后存盤在客戶端瀏覽器的 Cookies 中,也支持配置為 Server Side Session 實作,可以使用多種后端存盤,
在其它許多框架的 Session 實作中,都可以看到一個叫做 Flash 的特殊資料,用于實作輕量的通知反饋等,這個功能應該是 Rails 首創的,
另外,Rails 關于 Session 安全的檔案寫的非常非常細致,強烈建議閱讀,
Session 機制的使用
不同實作方式的對比
Session-Data 存盤在何處
Sever Side Session
Sever Side Session 是目前最常見的 Session 實作,以至于不少人會誤以為它是唯一一種 Session 實作,
優點:
- 資料存盤在服務端,相對來說安全性更強
- 請求時只需要傳遞 Session-ID,減小流量開銷
- 可以方便的吊銷 Session,管控 Session 策略
劣勢:
- Session-Data 集中管理,不利于并行化,需要專門解決 Session 共享問題
- Session-Data 需要占用服務端記憶體 / 存盤,對服務端存在壓力
Client Side Session
Client Side Session 相對小眾一些,但也可以看到許多框架都保留了對它的支持,Rails 甚至在檔案中專門寫了一大段來描述為什么自己默認這么用,
優點:
- 實作簡單,無須過多考慮 Session 存盤、查詢
- 將存盤壓力轉移到了客戶端,這樣可以減小服務端的資源消耗
- 多機并行時,不需要考慮 Session 共享問題,完美支持高并發
劣勢:
- Cookies 默認有 4KB 限制,不能存盤太多內容
- Cookies 會在每次請求時被默認攜帶,存盤較多內容時,增大了流量消耗
- 存在重放攻擊的風險,客戶端可能會將資料替換為合法的舊資料
- 實作 Session 資料拉黑、強制失效等功能時比較復雜
- 資料需要『簽名』或『加密』后才能確保安全,這需要開發者正確配置
- 部分實作只對資料進行了『簽名』,客戶端可以直接查看到資料內容,存在安全風險
Session-ID 如何進行傳遞
對于 Server Side Session 來說,絕大部分實作都會選擇將 Session-ID 放在 Cookies 中,在發起請求時自動帶上,這樣也是很符合直覺的做法,但是凡事都有例外,還是有一些特殊的實作,
重寫進 URL 中
例如,PHP 默認就支持拋開 Cookies,使用 URL 傳遞 Session-ID,相關的配置為 use_cookies 、 use_only_cookies 欄位,
在 Tomcat 6.0 中,也支持類似的功能,但似乎是通過當前會話是否存在 Cookies 來判斷的,也可以通過配置 disableURLRewriting 來關閉,
當然也存在不同的聲音,比如 Django 就明確拒絕這么做,為此還在檔案中專門寫了原因 ,其一是make URLs ugly,其二是不安全,
除此之外,在 WAP 時代,由于早期手機瀏覽器很多不支持 Cookie,這種行為也非常常見,
必須要說的是,這樣確實很不安全,比如可以參考這篇 9 年前的文章:淺談WAP網站安全 - 空虛浪子心 ,
放進 Header 中的其它欄位
其實前面在說到 Spring 的時候就有提到,它支持將 Session-ID 放進 Header 中的X-Auth-Token 欄位中,
實際上,只要前后端協調一致,其實放在任何一個欄位都是沒問題的,
放進隱藏的表單欄位
這種處理方式我只在資料中看到過,大意就是在輸出頁面時,為每一個 Form 都補一個隱藏的 Session-ID 欄位,
<form name="anyform" action="/xxx">
<input type="hidden" name="jsessionid" value=https://www.cnblogs.com/liqing/p/"ByOK2vjFD43aPnrF6C2HmdnV6QZcEbzWoWiBYEnLerjQ22zWpBng!-1582381342">
這種方式存在非常多的弊端,也難怪現在基本上看不到了,
Session-Data 的安全如何保障
對于 Client Side Session 來說,因為不需要索引,只需要看好 Session-Data 就好,在這件事上,大家也有不一樣的思路,
大部分實作都選擇只對 Session-Data 做『簽名』處理,這樣就算客戶端知道 Session-Data 內部的資料是什么,也無法簡單的篡改,
但是并不是所有的開發者都會注意這些,保不齊會有人存敏感資訊在里面,所以部分框架不但做了簽名,還做了『加密』,看也不讓看,
需要注意的是,這樣仍然存在重放攻擊的風險,用戶可以將其替換為舊的合法 Session-Data,此時服務端是無法鑒別出差異的,
另外,Rails 關于 Session 安全的檔案寫的非常非常細致,再次強烈建議閱讀,
Session 相關的誤解
Session 的概念并不復雜,洋洋灑灑寫了這么多,其實還是因為討論時遇到的各種誤解,這里我們復盤一下,
Session ID 就是 Session 嘛?
不能等同,Session-ID 只是 Server Side Session 實作中,常用的索引資訊,
Cookies 就是 Session 嘛?
Cookies 經常用于承載 Session 的資訊(ID 或 Data),但并不是必然情況,
Session 是不是認證方式?Session 和 Token 的關系是什么?
這是經常被誤解的一點,在此說下我個人的理解,
Token 并不是一個以技術手段定義的詞匯,它的定義來源于『用途』,
但凡用于認證鑒權的 憑據,我認為都可以稱作 Token ,與這個憑據的生成方式無關,
基于此,如果 Session-ID 或 Session-Data 被用作認證,那么我認為這條資料就可以被稱為 Token,
但是需要注意,Session 自身是個虛擬概念,與 Token 毫不相關,
Session 和 JWT 的關系是什么?
但凡提到 Session,就不可避免的會有人搬出 JWT ,也會有很多人把 Session 和 JWT 擺到對立面來討論,但是這兩個壓根就不在一個維度,
先來看看 JWT 到底是什么,它的全名是 JSON Web Token ,從名字就可以看出,它是特定的一種 Token 生成方式,
JWT 的生成方式大致是這樣:將資料按特定格式進行序列化,標記過期時間,對資料進行『簽名』后編碼為 URL Safe 的 Base64URL ,
誒?好像有點眼熟?這怎么和 Client Side Session 那邊對 Session-Data 的處理那么像呢?
除此之外,這里有一篇蠻有名的文章:Stop using JWT for sessions 也可以一起看看,
看完文章會發現,作者提到的那些將 JWT 用做 Session 實作的人,實際上說的都是** Client Side Session**啊,
那么問題變的比較清晰了:
- JWT 只是一種處理資料的手法,它通過簽名保證了資訊的不可篡改,特定的格式也具備其它一些小特性,
- JWT 的特性,讓它具備成為 Client Side Session 的資料處理方式的潛力,也確實有人這么做了,
而那些關于 JWT 與 Session 優劣的爭論,實際上大部分都是在爭論 Sever Side Session 和 Client Side Session 的優劣,
總結復盤
扯了冗長的上萬字,其實就是為了說明一個『定義』的問題,Session 本身并不復雜,只要不把各種簡稱搞混,
很多時候由于大量使用簡稱,或者模糊定義,導致我們對概念本身失去了掌握,也經常讓討論變的低效,
另一方面,不能把自己所熟悉的那些實作當作全部,時不時的看看其它語言或框架,也許能發現許多好玩兒的東西,
最后,如果本文有哪些地方出現了疏漏或錯誤,還請不吝賜教,一起交流進步,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/80915.html
標籤:其他
上一篇:6. 云停車 (其他篇)
下一篇:如何優雅地做系統錯誤提示?
