search
尋找貓咪~QQ 地點 桃園市桃園區 Taoyuan , Taoyuan

Flask 源碼解析:session

原文:

全文約 11700 字,讀完可能需要 18 分鐘。

這是 flask 源碼解析系列文章的其中一篇,本系列已發布文章列表:

session 簡介

在解析 session 的實現之前,我們先介紹一下 session 怎麼使用。session 可以看做是在不同的請求之間保存數據的方法,因為 HTTP 是無狀態的協議,但是在業務應用上我們希望知道不同請求是否是同一個人發起的。比如購物網站在用戶點擊進入購物車的時候,伺服器需要知道是哪個用戶執行了這個操作。

from flask import session

導入這個變數,在代碼中就能直接通過讀寫它和 session 交互。

  • from flask importFlask session escape request

  • app =Flask(__name__)

  • app.secret_key ='please-generate-a-random-secret_key'

  • @app.route("/")

  • def index:

  • if'username'in session:

  • return'hello, {}n'.format(escape(session['username']))

  • return'hello, strangern'

  • @app.route("/login" methods=['POST'])

  • def login:

  • session['username']= request.form['username']

  • return'login success'

  • if __name__ =='__main__':

  • app.run(host='0.0.0.0' port=5000 debug=True)

上面這段代碼模擬了一個非常簡單的登陸邏輯,用戶訪問

POST /login

來登陸,後面訪問頁面的時候

GET /

,會返回該用戶的名字。我們看一下具體的操作實例(下面的操作都是用 來執行的,使用

curl

直接訪問的話,我們可以看到返回 然後我們模擬登陸請求,

-v

是列印出請求,

-f

是告訴伺服器這是表單數據,

\--session=mysession

是把請求的 cookie 等信息保存到這個變數中,後面可以通過變數來指定 session:

  • ~-v -f --session=mysession POST http://127.0.0.1:5000/login username=cizixs

  • POST /login HTTP/1.1

  • Accept:*/*

  • Accept-Encoding: gzip, deflate

  • Content-Length: 15

  • Content-Type: application/x-www-form-urlencoded; charset=utf-8

  • Host: 127.0.0.1:5000

  • User-Agent: HTTPie/0.8.0

  • username=cizixs

  • HTTP/1.0 200 OK

  • Content-Length: 13

  • Content-Type: text/html; charset=utf-8

  • Date: Wed, 01 Mar 2017 04:20:54 GMT

  • Server: Werkzeug/0.11.2 Python/2.7.10

  • Set-Cookie: session=eyJ1c2VybmFtZSI6ImNpeml4cyJ9.C5fdpg.fqm3FTv0kYE2TuOyGF1mx2RuYQ4; HttpOnly; Path=/

  • login success

Set-Cookie

的頭部,cookie 的鍵是

session

,值是一堆看起來隨機的字元串。繼續,這個時候我們用 參數把這次的請求帶上保存在

mysession

中的信息,登陸后訪問,可以看到登陸的用戶名:

  • ~-v --session=mysession http://127.0.0.1:5000/

  • GET / HTTP/1.1

  • Accept:*/*

  • Accept-Encoding: gzip, deflate

  • Cookie: session=eyJ1c2VybmFtZSI6ImNpeml4cyJ9.C5fevg.LE03yEZDWTUMQW-nNkTr1zBEhKk

  • Host: 127.0.0.1:5000

  • User-Agent: HTTPie/0.8.0

  • HTTP/1.0 200 OK

  • Content-Length: 11

  • Content-Type: text/html; charset=utf-8

  • Date: Wed, 01 Mar 2017 04:25:46 GMT

  • Server: Werkzeug/0.11.2 Python/2.7.10

  • Set-Cookie: session=eyJ1c2VybmFtZSI6ImNpeml4cyJ9.C5feyg.sfFCDIqfef4i8cvxUClUUGQNcHA; HttpOnly; Path=/

  • hellocizixs

這次注意在發送的請求中,客戶端帶了

Cookie

頭部,上面的值保存了前一個請求的 response 給我們設置的值。

總結一下:session 是通過在客戶端設置 cookie 實現的,每次客戶端發送請求的時候會附帶著所有的 cookie,而裡面保存著一些重要的信息(比如這裡的用戶信息),這樣伺服器端就能知道客戶端的信息,然後根據這些數據做出對應的判斷,就好像不同請求之間是有記憶的。

解析

我們知道 session 是怎麼回事了,這部分就分析一下 flask 是怎麼實現它的。

請求過程

不難想象,session 的大致解析過程是這樣的:

  • 請求過來的時候,flask 會根據 cookie 信息創建出 session 變數(如果 cookie 不存在,這個變數有可能為空),保存在該請求的上下文中

  • 視圖函數可以獲取 session 中的信息,實現自己的邏輯處理

  • flask 會在發送 response 的時候,根據 session 的值,把它寫回到 cookie 中

注意:session 和 cookie 的轉化過程中,應該考慮到安全性,不然直接使用偽造的 cookie 會是個很大的安全隱患。

flask 上下文那篇文章

中,我們知道,每次請求過來的時候,我們訪問的

request

session

變數都是

RequestContext

實例的變數。在

RequestContext.Push

它初始化了

session

變數,保存在

RequestContext

上,這樣後面就能直接通過

from flaskimport session

來使用它。如果沒有設置

secret_key

變數,

open_session

就會返回 None,這個時候會調用

make_null_session

來生成一個空的 session,這個特殊的 session 不能進行任何讀寫操作,不然會報異常給用戶。我們來看看 在

Flask

中,所有和 session 有關的調用,都是轉發到

self.session_interface

的方法調用上(這樣用戶就能用自定義的

session_interface

來控制 session 的使用)。而默認的 後面遇到 session 有關方法解釋,我們會直接講解

SecureCookieSessionInterface

的代碼實現,跳過中間的這個轉發說明。

  • null_session_class =NullSession

  • def make_null_session(self app):

  • return self.null_session_class

  • def open_session(self app request):

  • # 獲取 session 簽名的演算法

  • s = self.get_signing_serializer(app)

  • if s isNone:

  • returnNone

  • # 從 cookie 中獲取 session 變數的值

  • val = request.cookies.get(app.session_cookie_name)

  • ifnot val:

  • return self.session_class

  • # 因為 cookie 的數據需要驗證是否有篡改,所以需要簽名演算法來讀取裡面的值

  • max_age = total_seconds(app.permanent_session_lifetime)

  • try:

  • data = s.loads(val max_age=max_age)

  • return self.session_class(data)

  • exceptBadSignature:

  • return self.session_class

open_session

根據請求中的 cookie 來獲取對應的 session 對象。之所以有

app

參數,是因為根據 app 中的安全設置(比如簽名演算法、secret_key)對 cookie 進行驗證。

這裡有兩點需要特殊說明的:簽名演算法是怎麼工作的?session 對象到底是怎麼定義的?

默認的 session 對象是

SecureCookieSession

,這個類就是一個基本的字典,外加一些特殊的屬性,比如

permanent

(flask 插件會用到這個變數)、

modified

(表明實例是否被更新過,如果更新過就要重新計算並設置 cookie,因為計算過程比較貴,所以如果對象沒有被修改,就直接跳過)。

  • classSessionMixin(object):

  • def _get_permanent(self):

  • return self.get('_permanent'False)

  • def _set_permanent(self value):

  • self['_permanent']= bool(value)

  • #: this reflects the ``'_permanent'`` key in the dict.

  • permanent = property(_get_permanent _set_permanent)

  • del _get_permanent _set_permanent

  • modified =True

  • classSecureCookieSession(CallbackDictSessionMixin):

  • """Base class for sessions based on signed cookies."""

  • def __init__(self initial=None):

  • def on_update(self):

  • self.modified =True

  • CallbackDict.__init__(self initial on_update)

  • self.modified =False

怎麼知道實例的數據被更新過呢? 是基於

werkzeug/datastructures:CallbackDict

實現的,這個類可以指定一個函數作為

on_update

參數,每次有字典操作的時候(

__setitem__

__delitem__

clear

popitem

update

pop

setdefault

)會調用這個函數。

NOTE

CallbackDict

的實現很巧妙,但是並不複雜,感興趣的可以自己參考代碼。主要思路就是重載字典的一些更新操作,讓它們在做原來事情的同時,額外調用一下實現保存的某個函數。對於開發者來說,可以把

session

簡單地看成字典,所有的操作都是和字典一致的。

簽名演算法

都獲取 cookie 數據的過程中,最核心的幾句話是:

  • s = self.get_signing_serializer(app)

  • val = request.cookies.get(app.session_cookie_name)

  • data = s.loads(val max_age=max_age)

  • return self.session_class(data)

其中兩句都和

s

有關,

signing_serializer

保證了 cookie 和 session 的轉換過程中的安全問題。如果 flask 發現請求的 cookie 被篡改了,它會直接放棄使用。我們繼續看 方法:

  • def get_signing_serializer(self app):

  • ifnot app.secret_key:

  • returnNone

  • signer_kwargs = dict(

  • key_derivation=self.key_derivation

  • digest_method=self.digest_method

  • )

  • returnURLSafeTimedSerializer(app.secret_key

  • salt=self.salt

  • serializer=self.serializer

  • signer_kwargs=signer_kwargs)

我們看到這裡需要用到很多參數:

  • secret_key:密鑰。這個是必須的,如果沒有配置 secret_key 就直接使用 session 會報錯

  • salt:為了增強安全性而設置一個 salt 字元串(可以自行搜索「安全加鹽」了解對應的原理)

  • serializer:序列演算法

  • signer_kwargs:其他參數,包括摘要/hash 演算法(默認是 sha1)和 簽名演算法(默認是 hmac

URLSafeTimedSerializeritsdangerous

庫的類,主要用來進行數據驗證,增加網路中數據的安全性。

itsdangerours

提供了多種

Serializer

,可以方便地進行類似 json 處理的數據序列化和反序列的操作。至於具體的實現,因為篇幅限制,就不解釋了。

應答過程

flask 會在請求過來的時候自動解析 cookie 的值,把它變成

session

變數。開發在視圖函數中可以使用它的值,也可以對它進行更新。最後再返回的 response 中,flask 也會自動把 session 寫回到 cookie。我們來看看這部分是怎麼實現的!之前的文章講解了應答的過程,其中

finalize_response

方法在根據視圖函數的返回生成 response 對象之後,會調用

process_response

方法進行處理。 這裡就是 session 在應答中出現的地方,思路也很簡單,如果需要就調用

save_sessoin

,把當前上下文中的

session

對象保存到 response 。

save_session

的代碼和

open_session
  • def save_session(self app session response):

  • domain = self.get_cookie_domain(app)

  • path = self.get_cookie_path(app)

  • # 如果 session 變成了空字典,flask 會直接刪除對應的 cookie

  • ifnot session:

  • if session.modified:

  • response.delete_cookie(app.session_cookie_name

  • domain=domain path=path)

  • return

  • # 是否需要設置 cookie。如果 session 發生了變化,就一定要更新 cookie,否則用戶可以 `SESSION_REFRESH_EACH_REQUEST` 變數控制是否要設置 cookie

  • ifnot self.should_set_cookie(app session):

  • return

  • = self.get_cookie_httponly(app)

  • secure = self.get_cookie_secure(app)

  • expires = self.get_expiration_time(app session)

  • val = self.get_signing_serializer(app).dumps(dict(session))

  • response.set_cookie(app.session_cookie_name val

  • expires=expires

  • =

  • domain=domain path=path secure=secure)

這段代碼也很容易理解,就是從

app

session

變數中獲取所有需要的信息,然後調用

response.set_cookie

設置最後的

cookie

。這樣客戶端就能在 cookie 中保存 session 有關的信息,以後訪問的時候再次發送給伺服器端,以此來實現有狀態的交互。解密 session有時候在開發或者調試的過程中,需要了解 cookie 中保存的到底是什麼值,可以通過手動解析它的值。

session

cookie

中的值,是一個字元串,由句號分割成三個部分。第一部分是

base64

加密的數據,第二部分是時間戳,第三部分是校驗信息。

前面兩部分的內容可以通過下面的方式獲取,代碼也可直觀,就不給出解釋了:

  • In[1]:from itsdangerous import*

  • In[2]: s ='eyJ1c2VybmFtZSI6ImNpeml4cyJ9.C5fdpg.fqm3FTv0kYE2TuOyGF1mx2RuYQ4'

  • In[3]: data timstamp secret = s.split('.')

  • In[4]: base64_decode(data)

  • Out[4]:'{"username":"cizixs"}'

  • In[5]: bytes_to_int(base64_decode(timstamp))

  • Out[5]:194502054

  • In[7]: time.strftime('%Y-%m-%d %H:%I%S' time.localtime(194502054 EPOCH))

  • Out[7]:'2017-03-01 12:1254'

總結

flask 默認提供的 session 功能還是很簡單的,滿足了基本的功能。但是我們看到 flask 把 session 的數據都保存在客戶端的 cookie 中,這裡只有用戶名還好,如果有一些私密的數據(比如密碼,賬戶餘額等等),就會造成嚴重的安全問題。可以考慮使用 flask-session 這個三方的庫,它把數據保存在伺服器端(本地文件、redis、memcached),客戶端只拿到一個 sessionid。

session 主要是用來在不同的請求之間保存信息,最常見的應用就是登陸功能。雖然直接通過

session

自己也可以寫出來不錯的登陸功能,但是在實際的項目中可以考慮

flask-login

這個三方的插件,方便我們的開發參考資料

  • flask-session github page

  • Flask 源碼閱讀筆記

題圖:pexels,CC0 授權。



熱門推薦

本文由 yidianzixun 提供 原文連結

寵物協尋 相信 終究能找到回家的路
寫了7763篇文章,獲得2次喜歡
留言回覆
回覆
精彩推薦