cookie 和 session 到底是什么
來源:labuladong
作者:labuladong
cookie 大家應(yīng)該都熟悉,比如說登錄某些網(wǎng)站一段時間后,就要求你重新登錄;再比如有的同學很喜歡玩爬蟲技術(shù),有時候網(wǎng)站就是可以攔截住你的爬蟲,這些都和 cookie 有關(guān)。
如果你明白了服務(wù)器后端對于 cookie 和 session 的處理邏輯,就可以解釋這些現(xiàn)象,甚至鉆一些空子無限白嫖,待我慢慢道來。
一、session 和 cookie 簡介
cookie 的出現(xiàn)是因為 HTTP 是無狀態(tài)的一種協(xié)議,換句話說,服務(wù)器記不住你,可能你每刷新一次網(wǎng)頁,就要重新輸入一次賬號密碼進行登錄。這顯然是讓人無法接受的,cookie 的作用就好比服務(wù)器給你貼個標簽,然后你每次向服務(wù)器再發(fā)請求時,服務(wù)器就能夠 cookie 認出你。
抽象地概括一下:一個 cookie 可以認為是一個「變量」,形如name=value,存儲在瀏覽器;一個 session 可以理解為一種數(shù)據(jù)結(jié)構(gòu),多數(shù)情況是「映射」(鍵值對),存儲在服務(wù)器上。
注意,我說的是「一個」cookie 可以認為是一個變量,但是服務(wù)器可以一次設(shè)置多個 cookie,所以有時候說 cookie 是「一組」鍵值對兒,這也可以說得通。
cookie 可以在服務(wù)器端通過 HTTP 的 SetCookie 字段設(shè)置 cookie,比如我用 Go 語言寫的一個簡單服務(wù):
func?cookie(w?http.ResponseWriter,?r?*http.Request)?{
????//?設(shè)置了兩個?cookie?
????http.SetCookie(w,?&http.Cookie{
????????Name:???????"name1",
????????Value:??????"value1",
????})
????http.SetCookie(w,?&http.Cookie{
????????Name:??"name2",
????????Value:?"value2",
????})
????//?將字符串寫入網(wǎng)頁
????fmt.Fprintln(w,?"頁面內(nèi)容")
}
當瀏覽器訪問對應(yīng)網(wǎng)址時,通過瀏覽器的開發(fā)者工具查看此次 HTTP 通信的細節(jié),可以看見服務(wù)器的回應(yīng)發(fā)出了兩次SetCookie命令:

在這之后,瀏覽器的請求中的Cookie字段就帶上了這兩個 cookie:

cookie 的作用其實就是這么簡單,無非就是服務(wù)器給每個客戶端(瀏覽器)打的標簽,方便服務(wù)器辨認而已。當然,HTTP 還有很多參數(shù)可以設(shè)置 cookie,比如過期時間,或者讓某個 cookie 只有某個特定路徑才能使用等等。
但問題是,我們也知道現(xiàn)在的很多網(wǎng)站功能很復(fù)雜,而且涉及很多的數(shù)據(jù)交互,比如說電商網(wǎng)站的購物車功能,信息量大,而且結(jié)構(gòu)也比較復(fù)雜,無法通過簡單的 cookie 機制傳遞這么多信息,而且要知道 cookie 字段是存儲在 HTTP header 中的,就算能夠承載這些信息,也會消耗很多的帶寬,比較消耗網(wǎng)絡(luò)資源。
session 就可以配合 cookie 解決這一問題,比如說一個 cookie 存儲這樣一個變量sessionID=xxxx,僅僅把這一個 cookie 傳給服務(wù)器,然后服務(wù)器通過這個 ID 找到對應(yīng)的 session,這個 session 是一個數(shù)據(jù)結(jié)構(gòu),里面存儲著該用戶的購物車等詳細信息,服務(wù)器可以通過這些信息返回該用戶的定制化網(wǎng)頁,有效解決了追蹤用戶的問題。
session 是一個數(shù)據(jù)結(jié)構(gòu),由網(wǎng)站的開發(fā)者設(shè)計,所以可以承載各種數(shù)據(jù),只要客戶端的 cookie 傳來一個唯一的 session ID,服務(wù)器就可以找到對應(yīng)的 session,認出這個客戶。
當然,由于 session 存儲在服務(wù)器中,肯定會消耗服務(wù)器的資源,所以 session 一般都會有一個過期時間,服務(wù)器一般會定期檢查并刪除過期的 session,如果后來該用戶再次訪問服務(wù)器,可能就會面臨重新登錄等等措施,然后服務(wù)器新建一個 session,將 session ID 通過 cookie 的形式傳送給客戶端。
那么,我們知道 cookie 和 session 的原理,有什么切實的好處呢?除了應(yīng)對面試,我給你說一個雞賊的用處,就是可以白嫖某些服務(wù)。
有些網(wǎng)站,你第一次使用它的服務(wù),它直接免費讓你試用,但是用一次之后,就讓你登錄然后付費繼續(xù)使用該服務(wù)。而且你發(fā)現(xiàn)網(wǎng)站似乎通過某些手段記住了你的電腦,除非你換個電腦或者換個瀏覽器才能再白嫖一次。
那么問題來了,你試用的時候沒有登錄,網(wǎng)站服務(wù)器是怎么記住你的呢?這就很顯然了,服務(wù)器一定是給你的瀏覽器打了 cookie,后臺建立了對應(yīng)的 session 記錄你的狀態(tài)。你的瀏覽器在每次訪問該網(wǎng)站的時候都會聽話地帶著 cookie,服務(wù)器一查 session 就知道這個瀏覽器已經(jīng)免費使用過了,得讓它登錄付費,不能讓它繼續(xù)白嫖了。
那如果我不讓瀏覽器發(fā)送 cookie,每次都偽裝成一個第一次來試用的小萌新,不就可以不斷白嫖了么?瀏覽器會把網(wǎng)站的 cookie 以文件的形式存在某些地方(不同的瀏覽器配置不同),你把他們找到然后刪除就行了。但是對于 Firefox 和 Chrome 瀏覽器,有很多插件可以直接編輯 cookie,比如我的 Chrome 瀏覽器就用的一款叫做 EditThisCookie 的插件,這是他們官網(wǎng):
http://www.editthiscookie.com/這類插件可以讀取瀏覽器在當前網(wǎng)頁的 cookie,點開插件可以任意編輯和刪除 cookie。
當然,偶爾白嫖一兩次還行,不鼓勵高頻率白嫖,想常用還是掏錢吧,否則網(wǎng)站賺不到錢,就只能取消免費試用這個機制了。
以上就是關(guān)于 cookie 和 session 的簡單介紹,cookie 是 HTTP 協(xié)議的一部分,不算復(fù)雜,而 session 是可以定制的,所以下面詳細看一下實現(xiàn) session 管理的代碼架構(gòu)吧。
二、session 的實現(xiàn)
session 的原理不難,但是具體實現(xiàn)它可是很有技巧的,一般需要三個組件配合完成,它們分別是Manager、Provider和Session三個類(接口)。
下圖就是一個 HTTP 請求的完整流程:

1、瀏覽器通過 HTTP 協(xié)議向服務(wù)器請求路徑/content的網(wǎng)頁資源,對應(yīng)路徑上有一個 Handler 函數(shù)接收請求,解析 HTTP header 中的 cookie,得到其中存儲的 sessionID,然后把這個 ID 發(fā)給Manager。
2、Manager充當一個 session 管理器的角色,主要存儲一些配置信息,比如 session 的存活時間,cookie 的名字等等。而所有的 session 存在一個Provider中。所以Manager會把sid(sessionID)傳遞給Provider,讓它去找這個 ID 對應(yīng)的具體是哪個 session。
3、Provider就是一個容器,最常見的應(yīng)該就是一個散列表,將每個sid和對應(yīng)的 session 一一映射起來。收到Manager傳遞的sid之后,它就找到sid對應(yīng)的 session 結(jié)構(gòu),也就是Session結(jié)構(gòu),然后返回它。
4、Session中存儲著用戶的具體信息,一般是一個散列表,由 Handler 函數(shù)中的邏輯拿出這些信息,生成該用戶的 HTML 網(wǎng)頁,返回給客戶端。
那么你也許會問,為什么搞這么麻煩,直接在 Handler 函數(shù)中搞一個哈希表,然后存儲sid和Session結(jié)構(gòu)的映射不就完事兒了?
這就是設(shè)計層面的技巧了,下面就來說說,為什么分成Manager、Provider和Session。
先從最底層的Session說。既然 session 就是鍵值對,為啥不直接用哈希表,而是要抽象出這么一個數(shù)據(jù)結(jié)構(gòu)呢?
第一,因為Session結(jié)構(gòu)可能不止存儲了一個哈希表,還可以存儲一些輔助數(shù)據(jù),比如sid,訪問次數(shù),過期時間或者最后一次的訪問時間,這樣便于實現(xiàn)想 LRU、LFU 這樣的算法。
第二,因為 session 可以有不同的存儲方式。如果用編程語言內(nèi)置的哈希表,那么 session 數(shù)據(jù)就是存儲在內(nèi)存中,如果數(shù)據(jù)量大,很容易造成程序崩潰,而且一旦程序結(jié)束,所有 session 數(shù)據(jù)都會丟失。所以可以有很多種 session 的存儲方式,比如存入緩存數(shù)據(jù)庫 Redis,或者存入 MySQL 等等。
因此,Session結(jié)構(gòu)提供一層抽象,屏蔽不同存儲方式的差異,只要提供一組通用接口操縱鍵值對:
type?Session?interface?{
????//?設(shè)置鍵值對
????Set(key,?val?interface{})
????//?獲取?key?對應(yīng)的值
????Get(key?interface{})?interface{}
????//?刪除鍵?key
????Delete(key?interface{})
}
再說Provider為啥要抽象出來。我們上面那個圖的Provider就是一個散列表,保存sid到Session的映射,但是實際中肯定會更加復(fù)雜。
我們不是要時不時刪除一些 session 嗎,除了設(shè)置存活時間之外,還可以采用一些其他策略,比如 LRU 緩存淘汰算法,這樣就需要Provider內(nèi)部使用哈希鏈表這種數(shù)據(jù)結(jié)構(gòu)來存儲 session。
PS:關(guān)于 LRU 算法的奧妙,參見前文 如何實現(xiàn) LRU 緩存機制。
因此,Provider作為一個容器,就是要屏蔽算法細節(jié),以合理的數(shù)據(jù)結(jié)構(gòu)和算法組織sid和Session的映射關(guān)系,只需要實現(xiàn)下面這幾個方法實現(xiàn)對 session 的增刪查改:
type?Provider?interface?{
????//?新增并返回一個?session
????SessionCreate(sid?string)?(Session,?error)
????//?刪除一個?session
????SessionDestroy(sid?string)
????//?查找一個?session
????SessionRead(sid?string)?(Session,?error)
????//?修改一個session
????SessionUpdate(sid?string)
????//?通過類似?LRU?的算法回收過期的?session
????SessionGC(maxLifeTime?int64)
}
最后說Manager,大部分具體工作都委托給Session和Provider承擔了,Manager主要就是一個配置參數(shù)集合,比如 session 的存活時間,清理過期 session 的策略,以及 session 的可用存儲方式。Manager屏蔽了操作的具體細節(jié),我們可以通過Manager靈活地配置 session 機制。
綜上,session 機制分成幾部分的最主要原因就是解耦,實現(xiàn)定制化。我在 Github 上看過幾個 Go 語言實現(xiàn)的 session 服務(wù),源碼都很簡單,有興趣的朋友可以學習學習:
https://github.com/alexedwards/scs
https://github.com/astaxie/build-web-application-with-golang
最后,如果本文對你有幫助,在看分享,就是對我最大的支持。
推薦閱讀
全部文章分類與整理(算法+數(shù)據(jù)結(jié)構(gòu)+計算機基礎(chǔ)),持續(xù)更新
