Cookie/Session的機制與安全

Cookie和Session是爲了在無狀態的HTTP協議之上維護會話狀態,使得服務器可以知道當前是和哪個客戶在打交道。本文來詳細討論Cookie和Session的實現機制,以及其中涉及的安全問題。

因爲HTTP協議是無狀態的,即每次用戶請求到達服務器時,HTTP服務器並不知道這個用戶是誰、是否登錄過等。現在的服務器之所以知道我們是否已經登錄,是因爲服務器在登錄時設置了瀏覽器的Cookie!Session則是藉由Cookie而實現的更高層的服務器與瀏覽器之間的會話。

Cookie是由網景公司的前僱員Lou Montulli在1993年發明的,現今Cookie已經廣泛使用了。

cookie

Cookie 的實現機制

Cookie是由客戶端保存的小型文本文件,其內容爲一系列的鍵值對。 Cookie是由HTTP服務器設置的,保存在瀏覽器中, 在用戶訪問其他頁面時,會在HTTP請求中附上該服務器之前設置的Cookie。 Cookie的實現標準定義在RFC2109: HTTP State Management Mechanism中。 那麼Cookie是怎樣工作的呢?下面給出整個Cookie的傳遞流程:

  1. 瀏覽器向某個URL發起HTTP請求(可以是任何請求,比如GET一個頁面、POST一個登錄表單等)
  2. 對應的服務器收到該HTTP請求,並計算應當返回給瀏覽器的HTTP響應。

    HTTP響應包括請求頭和請求體兩部分,可以參見:讀 HTTP 協議

  3. 在響應頭加入Set-Cookie字段,它的值是要設置的Cookie。

    RFC2109 6.3 Implementation Limits中提到: UserAgent(瀏覽器就是一種用戶代理)至少應支持300項Cookie, 每項至少應支持到4096字節,每個域名至少支持20項Cookie。

  4. 瀏覽器收到來自服務器的HTTP響應。
  5. 瀏覽器在響應頭中發現Set-Cookie字段,就會將該字段的值保存在內存或者硬盤中。

    Set-Cookie字段的值可以是很多項Cookie,每一項都可以指定過期時間Expires。 默認的過期時間是用戶關閉瀏覽器時。

  6. 瀏覽器下次給該服務器發送HTTP請求時, 會將服務器設置的Cookie附加在HTTP請求的頭字段Cookie中。

    瀏覽器可以存儲多個域名下的Cookie,但只發送當前請求的域名曾經指定的Cookie, 這個域名也可以在Set-Cookie字段中指定)。

  7. 服務器收到這個HTTP請求,發現請求頭中有Cookie字段, 便知道之前就和這個用戶打過交道了。

  8. 過期的Cookie會被瀏覽器刪除。

總之,服務器通過Set-Cookie響應頭字段來指示瀏覽器保存Cookie, 瀏覽器通過Cookie請求頭字段來告訴服務器之前的狀態。 Cookie中包含若干個鍵值對,每個鍵值對可以設置過期時間。

Cookie 的安全隱患

Cookie提供了一種手段使得HTTP請求可以附加當前狀態, 現今的網站也是靠Cookie來標識用戶的登錄狀態的:

  1. 用戶提交用戶名和密碼的表單,這通常是一個POST HTTP請求。
  2. 服務器驗證用戶名與密碼,如果合法則返回200(OK)並設置Set-Cookieauthed=true
  3. 瀏覽器存儲該Cookie。
  4. 瀏覽器發送請求時,設置Cookie字段爲authed=true
  5. 服務器收到第二次請求,從Cookie字段得知該用戶已經登錄。 按照已登錄用戶的權限來處理此次請求。

這裏面的問題在哪裏?

我們知道可以發送HTTP請求的不只是瀏覽器,很多HTTP客戶端軟件(包括curl、Node.js)都可以發送任意的HTTP請求,可以設置任何頭字段。 假如我們直接設置Cookie字段爲authed=true併發送該HTTP請求, 服務器豈不是被欺騙了?這種攻擊非常容易,Cookie是可以被篡改的!

Cookie 防篡改機制

服務器可以爲每個Cookie項生成簽名,由於用戶篡改Cookie後無法生成對應的簽名, 服務器便可以得知用戶對Cookie進行了篡改。一個簡單的校驗過程可能是這樣的:

  1. 在服務器中配置一個不爲人知的字符串(我們叫它Secret),比如:x$sfz32
  2. 當服務器需要設置Cookie時(比如authed=false),不僅設置authed的值爲false, 在值的後面進一步設置一個簽名,最終設置的Cookie是authed=false|6hTiBl7lVpd1P
  3. 簽名6hTiBl7lVpd1P是這樣生成的:Hash('x$sfz32'+'true')。 要設置的值與Secret相加再取哈希。
  4. 用戶收到HTTP響應並發現頭字段Set-Cookie: authed=false|6hTiBl7lVpd1P
  5. 用戶在發送HTTP請求時,篡改了authed值,設置頭字段Cookie: authed=true|???。 因爲用戶不知道Secret,無法生成簽名,只能隨便填一個。
  6. 服務器收到HTTP請求,發現Cookie: authed=true|???。服務器開始進行校驗: Hash('true'+'x$sfz32'),便會發現用戶提供的簽名不正確。

通過給Cookie添加簽名,使得服務器得以知道Cookie被篡改。然而故事並未結束。

因爲Cookie是明文傳輸的, 只要服務器設置過一次authed=true|xxxx我不就知道true的簽名是xxxx了麼, 以後就可以用這個簽名來欺騙服務器了。因此Cookie中最好不要放敏感數據。 一般來講Cookie中只會放一個Session Id,而Session存儲在服務器端。

Session 的實現機制

Session 是存儲在服務器端的,避免了在客戶端Cookie中存儲敏感數據。 Session 可以存儲在HTTP服務器的內存中,也可以存在內存數據庫(如redis)中, 對於重量級的應用甚至可以存儲在數據庫中。

我們以存儲在redis中的Session爲例,還是考察如何驗證用戶登錄狀態的問題。

  1. 用戶提交包含用戶名和密碼的表單,發送HTTP請求。
  2. 服務器驗證用戶發來的用戶名密碼。
  3. 如果正確則把當前用戶名(通常是用戶對象)存儲到redis中,並生成它在redis中的ID。

    這個ID稱爲Session ID,通過Session ID可以從Redis中取出對應的用戶對象, 敏感數據(比如authed=true)都存儲在這個用戶對象中。

  4. 設置Cookie爲sessionId=xxxxxx|checksum併發送HTTP響應, 仍然爲每一項Cookie都設置簽名。
  5. 用戶收到HTTP響應後,便看不到任何敏感數據了。在此後的請求中發送該Cookie給服務器。
  6. 服務器收到此後的HTTP請求後,發現Cookie中有SessionID,進行放篡改驗證。
  7. 如果通過了驗證,根據該ID從Redis中取出對應的用戶對象, 查看該對象的狀態並繼續執行業務邏輯。

Web應用框架都會實現上述過程,在Web應用中可以直接獲得當前用戶。 相當於在HTTP協議之上,通過Cookie實現了持久的會話。這個會話便稱爲Session。

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