1. 程式人生 > 實用技巧 >一文看懂 session 和 cookie

一文看懂 session 和 cookie

-----------

cookie 大家應該都熟悉,比如說登入某些網站一段時間後,就要求你重新登入;再比如有的同學很喜歡玩爬蟲技術,有時候網站就是可以攔截住你的爬蟲,這些都和 cookie 有關。如果你明白了伺服器後端對於 cookie 和 session 的處理邏輯,就可以解釋這些現象,甚至鑽一些空子無限白嫖,待我慢慢道來。

cookie 的出現是因為 HTTP 是無狀態的一種協議,換句話說,伺服器記不住你,可能你每重新整理一次網頁,就要重新輸入一次賬號密碼進行登入。這顯然是讓人無法接受的,cookie 的作用就好比伺服器給你貼個標籤,然後你每次向伺服器再發請求時,伺服器就能夠 cookie 認出你。

抽象地概括一下:一個 cookie 可以認為是一個「變數」,形如 name=value,儲存在瀏覽器;一個 session 可以理解為一種資料結構,多數情況是「對映」(鍵值對),儲存在伺服器上

注意,我說的是「一個」cookie 可以認為是一個變數,但是伺服器可以一次設定多個 cookie,所以有時候說 cookie 是「一組」鍵值對兒,這也可以說得通。

cookie 可以在伺服器端通過 HTTP 的 SetCookie 欄位設定 cookie,比如我用 Go 語言寫的一個簡單服務:

func cookie(w http.ResponseWriter, r *http.Request) {
    // 設定了兩個 cookie 
	http.SetCookie(w, &http.Cookie{
		Name:       "name1",
		Value:      "value1",
	})

	http.SetCookie(w, &http.Cookie{
		Name:  "name2",
		Value: "value2",
	})
    // 將字串寫入網頁
	fmt.Fprintln(w, "頁面內容")
}

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。

當瀏覽器訪問對應網址時,通過瀏覽器的開發者工具檢視此次 HTTP 通訊的細節,可以看見伺服器的迴應發出了兩次 SetCookie 命令:

在這之後,瀏覽器的請求中的 Cookie 欄位就帶上了這兩個 cookie:

cookie 的作用其實就是這麼簡單,無非就是伺服器給每個客戶端(瀏覽器)打的標籤,方便伺服器辨認而已。當然,HTTP 還有很多引數可以設定 cookie,比如過期時間,或者讓某個 cookie 只有某個特定路徑才能使用等等。

但問題是,我們也知道現在的很多網站功能很複雜,而且涉及很多的資料互動,比如說電商網站的購物車功能,資訊量大,而且結構也比較複雜,無法通過簡單的 cookie 機制傳遞這麼多資訊,而且要知道 cookie 欄位是儲存在 HTTP header 中的,就算能夠承載這些資訊,也會消耗很多的頻寬,比較消耗網路資源。

session 就可以配合 cookie 解決這一問題,比如說一個 cookie 儲存這樣一個變數 sessionID=xxxx,僅僅把這一個 cookie 傳給伺服器,然後伺服器通過這個 ID 找到對應的 session,這個 session 是一個數據結構,裡面儲存著該使用者的購物車等詳細資訊,伺服器可以通過這些資訊返回該使用者的定製化網頁,有效解決了追蹤使用者的問題。

session 是一個數據結構,由網站的開發者設計,所以可以承載各種資料,只要客戶端的 cookie 傳來一個唯一的 session ID,伺服器就可以找到對應的 session,認出這個客戶。

當然,由於 session 儲存在伺服器中,肯定會消耗伺服器的資源,所以 session 一般都會有一個過期時間,伺服器一般會定期檢查並刪除過期的 session,如果後來該使用者再次訪問伺服器,可能就會面臨重新登入等等措施,然後伺服器新建一個 session,將 session ID 通過 cookie 的形式傳送給客戶端。

那麼,我們知道 cookie 和 session 的原理,有什麼切實的好處呢?除了應對面試,我給你說一個雞賊的用處,就是可以白嫖某些服務

有些網站,你第一次使用它的服務,它直接免費讓你試用,但是用一次之後,就讓你登入然後付費繼續使用該服務。而且你發現網站似乎通過某些手段記住了你的電腦,除非你換個電腦或者換個瀏覽器才能再白嫖一次。

PS:我認真寫了 100 多篇原創,手把手刷 200 道力扣題目,全部發布在 labuladong的演算法小抄,持續更新。建議收藏,按照我的文章順序刷題,掌握各種演算法套路後投再入題海就如魚得水了。

那麼問題來了,你試用的時候沒有登入,網站伺服器是怎麼記住你的呢?這就很顯然了,伺服器一定是給你的瀏覽器打了 cookie,後臺建立了對應的 session 記錄你的狀態。你的瀏覽器在每次訪問該網站的時候都會聽話地帶著 cookie,伺服器一查 session 就知道這個瀏覽器已經免費使用過了,得讓它登入付費,不能讓它繼續白嫖了。

那如果我不讓瀏覽器傳送 cookie,每次都偽裝成一個第一次來試用的小萌新,不就可以不斷白嫖了麼?瀏覽器會把網站的 cookie 以檔案的形式存在某些地方(不同的瀏覽器配置不同),你把他們找到然後刪除就行了。但是對於 Firefox 和 Chrome 瀏覽器,有很多外掛可以直接編輯 cookie,比如我的 Chrome 瀏覽器就用的一款叫做 EditThisCookie 的外掛,這是他們官網:

這類外掛可以讀取瀏覽器在當前網頁的 cookie,點開外掛可以任意編輯和刪除 cookie。當然,偶爾白嫖一兩次還行,不鼓勵高頻率白嫖,想常用還是掏錢吧,否則網站賺不到錢,就只能取消免費試用這個機制了

以上就是關於 cookie 和 session 的簡單介紹,cookie 是 HTTP 協議的一部分,不算複雜,而 session 是可以定製的,所以下面詳細看一下實現 session 管理的程式碼架構吧。

二、session 的實現

session 的原理不難,但是具體實現它可是很有技巧的,一般需要三個元件配合完成,它們分別是 ManagerProviderSession 三個類(介面)。

1、瀏覽器通過 HTTP 協議向伺服器請求路徑 /content 的網頁資源,對應路徑上有一個 Handler 函式接收請求,解析 HTTP header 中的 cookie,得到其中儲存的 sessionID,然後把這個 ID 發給 Manager

2、Manager 充當一個 session 管理器的角色,主要儲存一些配置資訊,比如 session 的存活時間,cookie 的名字等等。而所有的 session 存在 Manager 內部的一個 Provider 中。所以 Manager 會把 sid(sessionID)傳遞給 Provider,讓它去找這個 ID 對應的具體是哪個 session。

3、Provider 就是一個容器,最常見的應該就是一個散列表,將每個 sid 和對應的 session 一一對映起來。收到 Manager 傳遞的 sid 之後,它就找到 sid 對應的 session 結構,也就是 Session 結構,然後返回它。

4、Session 中儲存著使用者的具體資訊,由 Handler 函式中的邏輯拿出這些資訊,生成該使用者的 HTML 網頁,返回給客戶端。

那麼你也許會問,為什麼搞這麼麻煩,直接在 Handler 函式中搞一個雜湊表,然後儲存 sidSession 結構的對映不就完事兒了?

這就是設計層面的技巧了,下面就來說說,為什麼分成 ManagerProviderSession

先從最底層的 Session 說。既然 session 就是鍵值對,為啥不直接用雜湊表,而是要抽象出這麼一個數據結構呢?

第一,因為 Session 結構可能不止儲存了一個雜湊表,還可以儲存一些輔助資料,比如 sid,訪問次數,過期時間或者最後一次的訪問時間,這樣便於實現想 LRU、LFU 這樣的演算法。

第二,因為 session 可以有不同的儲存方式。如果用程式語言內建的雜湊表,那麼 session 資料就是儲存在記憶體中,如果資料量大,很容易造成程式崩潰,而且一旦程式結束,所有 session 資料都會丟失。所以可以有很多種 session 的儲存方式,比如存入快取資料庫 Redis,或者存入 MySQL 等等。

因此,Session 結構提供一層抽象,遮蔽不同儲存方式的差異,只要提供一組通用介面操縱鍵值對:

type Session interface {
    // 設定鍵值對
    Set(key, val interface{})
    // 獲取 key 對應的值
    Get(key interface{}) interface{}
    // 刪除鍵 key
	Delete(key interface{})
}

再說 Provider 為啥要抽象出來。我們上面那個圖的 Provider 就是一個散列表,儲存 sidSession 的對映,但是實際中肯定會更加複雜。我們不是要時不時刪除一些 session 嗎,除了設定存活時間之外,還可以採用一些其他策略,比如 LRU 快取淘汰演算法,這樣就需要 Provider 內部使用雜湊連結串列這種資料結構來儲存 session。

PS:關於 LRU 演算法的奧妙,參見前文「LRU 演算法詳解」。

因此,Provider 作為一個容器,就是要遮蔽演算法細節,以合理的資料結構和演算法組織 sidSession 的對映關係,只需要實現下面這幾個方法實現對 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,大部分具體工作都委託給 SessionProvider 承擔了,Manager 主要就是一個引數集合,比如 session 的存活時間,清理過期 session 的策略,以及 session 的可用儲存方式。Manager 遮蔽了操作的具體細節,我們可以通過 Manager 靈活地配置 session 機制。

綜上,session 機制分成幾部分的最主要原因就是解耦,實現定製化。我在 Github 上看過幾個 Go 語言實現的 session 服務,原始碼都很簡單,有興趣的朋友可以學習學習:

https://github.com/alexedwards/scs

https://github.com/astaxie/build-web-application-with-golang

_____________

我的 線上電子書 有 100 篇原創文章,手把手帶刷 200 道力扣題目,建議收藏!對應的 GitHub 演算法倉庫 已經獲得了 70k star,歡迎標星!