瞭解Cookie、Session和Token

在Web剛興起的階段,Web服務都是靜態服務,一般處理前端請求,只需要將相應的html、圖片等文件傳送到前端即可,這個時候,對於同一個請求,每個用戶看到的內容都是完全一樣的,服務器也沒必要針對不同用戶做不同的處理。Http協議最開始就是定義服務器和客戶端傳送超文本文件的協議,從起源上看,Http協議就是無狀態的。

但是隨着交互式Web應用的興起,之前無狀態的Http協議就不能滿足了。比如購物、論壇等場景,服務器需要知道當前請求是否已登陸、用戶在購物車添加了那些商品,針對不同的請求,要做不同的處理,之前無狀態很明顯不能滿足這種需求了。

解決方案就是給每個客戶端頒發一個會話標識(session id),說白了就是一個隨機的字符串,每個客戶端收到的都不一樣,每次客戶端向服務端發起HTTP請求時,會把這個字符串給一併帶過來, 這樣服務端就能區分開誰是誰了。而本文講的Cookie、Session和Token就是用來解決Http無狀態的不同方式。

1. Cookie

1.1 基本概念

上面講到,爲了服務端能區分不同的客戶端請求,會給每一個客戶端頒發一個會話標識(Session Id)。這個會話標識有很多方式可以發送給客戶端,而Cookie就是其中一種比較常見的方式。

Cookie是客戶端保存用戶信息的一種機制,用來記錄用戶的一些信息,也是實現Session的一種方式(存儲session id)。Cookie總是保存在客戶端中,按在客戶端中的存儲位置,可分爲內存Cookie和硬盤Cookie。內存Cookie由瀏覽器維護,保存在內存中,瀏覽器關閉後就消失了,其存在時間是短暫的。硬盤Cookie保存在硬盤裏,有一個過期時間,除非用戶手工清理或到了過期時間,硬盤Cookie不會被刪除,其存在時間是長期的。內存Cookie和硬盤Cookie也可以叫做非持久Cookie和持久Cookie。

Cookie存儲的數據量有限,不同的瀏覽器有不同的存儲大小,但一般不超過4KB。因此使用Cookie實際上只能存儲一小段的文本信息。例如:輸入用戶名密碼登錄網站後,第二天再打開很多情況下就直接打開了,這個時候用到的一個機制就是Cookie。

1.2 Cookie的缺陷

  • Cookie會被附加在每個HTTP請求中,所以無形中增加了流量。
  • 由於在HTTP請求中的Cookie是明文傳遞的,所以安全性成問題,除非用HTTPS
  • Cookie的大小限制在4KB左右,對於複雜的存儲需求來說是不夠用的。
  • Cookie有可能會被XSS攻擊獲取,如果Cookie中存儲的是用戶的SessionId,那麼攻擊者就可以通過合法身份操作用戶賬戶,有一定的安全隱患。

2. Session

2.1 基本概念

上面講到爲了使服務器可以區分不同的客戶端的請求,可以給每個客戶端一個獨立的會話標識(Session Id)方式來實現。那麼仔細想一下,如果只使用這個sessionId能不能達到目的?比如如果服務端沒有記錄這個Session Id對應的信息,服務端怎麼知道這個Session Id是自己頒發的?假如服務端不額外記錄任何信息,我們想象一下登陸的場景,如果想實現下次用戶訪問不用重新登陸,那必然要把用戶信息的信息(用戶名,密碼)存儲在Cookie中傳給客戶端,然後客戶端每次請求把Cookie帶給服務端,服務端在用戶不感知的情況下去校驗,但上面也講過Cookie中存儲密碼等敏感信息是不合理的。

所以到這裏,我們可以得出一個結論,爲了使服務器可以區分不同客戶端的請求,服務端必須額外也維護一些信息,這個信息跟頒發給客戶端的標識(Session Id)是一一對應的。而服務端額外維護的信息就是Session。

Session在服務端保存一個數據結構(主要存儲Session Id和Session內容,同時也包含了很多自定義的內容如:用戶基礎信息、權限信息),這個數據可以保存在內存、數據庫、文件中,Redis等介質中。當客戶端瀏覽器訪問服務器的時候,服務器把客戶端信息以某種形式記錄在上述介質中,並把Session Id通過Cookie給回客戶端。客戶端瀏覽器再次訪問時只需將Cookie中的SessionId給回服務端,服務端就可以根據Session Id和自己存儲的Session信息,來確定客戶端身份了。

比如用戶第一次登錄後,瀏覽器會將用戶信息發送給服務器,服務器校驗通過後,會爲該用戶創建一個SessionId,並在響應內容(Cookie)中將該Session Id一併返回給瀏覽器,瀏覽器將這些數據保存在本地。當用戶再次發送請求時,瀏覽器會自動的把上次請求存儲的Cookie數據自動的攜帶給服務器。服務器接收到請求信息後,會通過瀏覽器請求的數據中的Session Id判斷當前是哪個用戶,然後根據Session Id在Session庫中獲取用戶的Session數據返回給瀏覽器。

Session生成後,只要用戶繼續訪問,服務器就會更新Session的最後訪問時間,並維護該Session。一般服務器會給Session維護一個超時時間,過了超時時間,服務器就會把這個Session刪除。如果過了超時時間再訪問過服務器,Session就自動失效了。

2.1 Session的缺陷

在用戶量比較少時,上述這種維護用戶狀態的方式是沒什麼問題的。但是用戶量一旦達到一定規模,就會對服務起產生比較大的壓力。比如服務起將Session存儲在內存中,用戶量達到千萬級別,服務起就要維護千萬個用戶的Session,這對服務起而言本來就是一個比較大的挑戰。

除此之外,Session的存在,也限制了服務器的水平擴展能力。比如用戶規模變大後,服務端做水平擴展,新增了幾臺服務實例。此時用戶1通過服務器A登陸了系統,那麼用戶1的Session就會保存在機器A上,假如用戶1下一次的請求被轉發到機器B上,就會出現問題,因爲機器B上沒有用戶1的Session。

針對上述這個問題,也有解決方案,比如session sticky。就是通過負載均衡服務器的策略,將用戶1的請求只轉發到機器A上,就能解決上述問題了。但是也會引入一個新的問題,對於單個用戶而言,一旦處理該用戶的服務器掛了,還是要將用戶請求轉發到其他機器上,還是會出現Session丟失的問題。所以爲了徹底解決這個問題,就只能在不同服務器之間進行Session複製,那麼又回到最開始的狀態了,每個服務器都要維護大量的Session,對服務器而言是個很大的負擔。

如果不把Session保存在內存,而藉助Redis、Memcached等統一管理,也能解決Session複製問題,如下:

但是,引入Redis、Memcached也會出現另一個問題,就是存儲介質的單點問題,如果Redis或者Memcached掛了,那麼所有的Session都會丟失。雖然可以使用存儲介質集羣化的方式,提高可用行,但是額外維護Session,對於服務端而言確實是一個負擔。

3. Token

3.1 基本概念

上面講Session時講到,服務端單獨存儲Session,對服務端而言是一個負擔。那有沒有一種方式可以讓服務端不用單獨維護Session?在上面也講過,如果服務端不維護Session,那麼就無法確認客戶端帶過來的Session Id是否是自己頒發的。這樣不懷好意的人僞造一個Session Id給服務器,服務器也是無法辨別的。那有沒有一種方式,可以在服務端不額外維護Session的前提下,識別客戶端送過來的校驗信息的合法性?(比如是否是自己頒發的,有沒有被惡意改動過)答案是有的,就是接下來要講的Token。

比如用戶1已經登錄了系統, 服務器會給他發一個令牌(Token), 裏邊包含了用戶1的user id, 下一次用戶1再次請求服務器的時候, 把這個Token通過Http header 帶給服務器就可以了。那麼服務器是如何驗證Token的合法性的,我們來看一下Token的生成規則。

首先對要傳送到客戶端的數據做一個簽名,比如使用HMAC-SHA256算法,加上一個只有我才知道的密鑰,對數據做一個簽名, 把這個簽名和數據一起作爲Token,由於密鑰別人不知道,就無法僞造Token了。

這個Token服務端並不保存,用戶1把這個Token發給服務器的時候,服務器再用同樣的HMAC-SHA256算法和密鑰,對數據再計算一次簽名, 和Token中的簽名做個比較, 如果相同,服務器就知道用戶1已經登錄過了,並且可以直接取到用戶1的user id , 如果不相同,數據部分肯定被人篡改過,直接拒絕服務。

Token中的數據是明文保存的(雖然會用Base64做下編碼,但那不是加密),還是可以被別人看到的,所以不能在其中保存像密碼這樣的敏感信息。另外,如果一個人的Token被別人偷走了,那服務器也會認爲小偷就是合法用戶, 這其實和一個人的Session id被別人偷走是一樣的。

這樣一來,服務器就不用額外保存Session了, 只需要生成Token,然後驗證Token,相當於用CPU計算時間換取了Session存儲空間。

參考鏈接:

1. 《碼農翻身——幹掉狀態》

2. 維基百科——Cookie

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章