一文看懂 session 和 cookie

-----------

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

一、session 和 cookie 簡介

cookie 的出現(xiàn)是因為 HTTP 是無狀態(tài)的一種協(xié)議,換句話說,服務器記不住你,可能你每刷新一次網頁,就要重新輸入一次賬號密碼進行登錄。這顯然是讓人無法接受的,cookie 的作用就好比服務器給你貼個標簽,然后你每次向服務器再發(fā)請求時,服務器就能夠 cookie 認出你。

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

注意,我說的是「一個」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 多篇原創(chuàng),手把手刷 200 道力扣題目,全部發(fā)布在 labuladong的算法小抄,持續(xù)更新。建議收藏,按照我的文章順序刷題,掌握各種算法套路后投再入題海就如魚得水了。

當瀏覽器訪問對應網址時,通過瀏覽器的開發(fā)者工具查看此次 HTTP 通信的細節(jié),可以看見服務器的回應發(fā)出了兩次 SetCookie 命令:

image

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

image

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

但問題是,我們也知道現(xiàn)在的很多網站功能很復雜,而且涉及很多的數(shù)據(jù)交互,比如說電商網站的購物車功能,信息量大,而且結構也比較復雜,無法通過簡單的 cookie 機制傳遞這么多信息,而且要知道 cookie 字段是存儲在 HTTP header 中的,就算能夠承載這些信息,也會消耗很多的帶寬,比較消耗網絡資源。

session 就可以配合 cookie 解決這一問題,比如說一個 cookie 存儲這樣一個變量 sessionID=xxxx,僅僅把這一個 cookie 傳給服務器,然后服務器通過這個 ID 找到對應的 session,這個 session 是一個數(shù)據(jù)結構,里面存儲著該用戶的購物車等詳細信息,服務器可以通過這些信息返回該用戶的定制化網頁,有效解決了追蹤用戶的問題。

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

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

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

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

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

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

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

http://www.editthiscookie.com/

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

以上就是關于 cookie 和 session 的簡單介紹,cookie 是 HTTP 協(xié)議的一部分,不算復雜,而 session 是可以定制的,所以下面詳細看一下實現(xiàn) session 管理的代碼架構吧。

二、session 的實現(xiàn)

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

image

1、瀏覽器通過 HTTP 協(xié)議向服務器請求路徑 /content 的網頁資源,對應路徑上有一個 Handler 函數(shù)接收請求,解析 HTTP header 中的 cookie,得到其中存儲的 sessionID,然后把這個 ID 發(fā)給 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 函數(shù)中的邏輯拿出這些信息,生成該用戶的 HTML 網頁,返回給客戶端。

那么你也許會問,為什么搞這么麻煩,直接在 Handler 函數(shù)中搞一個哈希表,然后存儲 sidSession 結構的映射不就完事兒了?

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

先從最底層的 Session 說。既然 session 就是鍵值對,為啥不直接用哈希表,而是要抽象出這么一個數(shù)據(jù)結構呢?

第一,因為 Session 結構可能不止存儲了一個哈希表,還可以存儲一些輔助數(shù)據(jù),比如 sid,訪問次數(shù),過期時間或者最后一次的訪問時間,這樣便于實現(xiàn)想 LRU、LFU 這樣的算法。

第二,因為 session 可以有不同的存儲方式。如果用編程語言內置的哈希表,那么 session 數(shù)據(jù)就是存儲在內存中,如果數(shù)據(jù)量大,很容易造成程序崩潰,而且一旦程序結束,所有 session 數(shù)據(jù)都會丟失。所以可以有很多種 session 的存儲方式,比如存入緩存數(shù)據(jù)庫 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 內部使用哈希鏈表這種數(shù)據(jù)結構來存儲 session。

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

因此,Provider 作為一個容器,就是要屏蔽算法細節(jié),以合理的數(shù)據(jù)結構和算法組織 sidSession 的映射關系,只需要實現(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,大部分具體工作都委托給 SessionProvider 承擔了,Manager 主要就是一個參數(shù)集合,比如 session 的存活時間,清理過期 session 的策略,以及 session 的可用存儲方式。Manager 屏蔽了操作的具體細節(jié),我們可以通過 Manager 靈活地配置 session 機制。

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

https://github.com/alexedwards/scs

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

_____________

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

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

友情鏈接更多精彩內容