這是 flask 原始碼解析系列文章的其中一篇,本系列所有文章列表:
- flask 原始碼解析:簡介
- flask 原始碼解析:應用啟動流程
- flask 原始碼解析:路由
- flask 原始碼解析:上下文
- flask 原始碼解析:請求
- flask 原始碼解析:響應
- flask 原始碼解析:session
session 簡介
在解析 session 的實現之前,我們先介紹一下 session 怎麼使用。session 可以看做是在不同的請求之間儲存資料的方法,因為 HTTP 是無狀態的協議,但是在業務應用上我們希望知道不同請求是否是同一個人發起的。比如購物網站在使用者點選進入購物車的時候,伺服器需要知道是哪個使用者執行了這個操作。
在 flask 中使用 session 也很簡單,只要使用 from flask import session
匯入這個變數,在程式碼中就能直接通過讀寫它和 session 互動。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
from flask import Flask, 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 /
,會返回該使用者的名字。我們看一下具體的操作例項(下面的操作都是用 httpie 來執行的,使用 curl
命令也能達到相同的效果):
直接訪問的話,我們可以看到返回 hello stranger
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
➜ ~ http -v http://127.0.0.1:5000/ GET / HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Host: 127.0.0.1:5000 User-Agent: HTTPie/0.8.0 HTTP/1.0 200 OK Content-Length: 14 Content-Type: text/html; charset=utf-8 Date: Wed, 01 Mar 2017 04:22:18 GMT Server: Werkzeug/0.11.2 Python/2.7.10 hello stranger |
然後我們模擬登陸請求,-v
是列印出請求,-f
是告訴伺服器這是表單資料,--session=mysession
是把請求的 cookie 等資訊儲存到這個變數中,後面可以通過變數來指定 session:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
➜ ~ http -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 |
最重要的是我們看到 response 中有 Set-Cookie
的頭部,cookie 的鍵是 session
,值是一堆看起來隨機的字串。
繼續,這個時候我們用 --session=mysession
引數把這次的請求帶上儲存在 mysession
中的資訊,登陸後訪問,可以看到登陸的使用者名稱:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
➜ ~ http -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()
方法的最後有這麼一段程式碼:
1 2 3 4 |
self.session = self.app.open_session(self.request) if self.session is None: self.session = self.app.make_null_session() |
它初始化了 session
變數,儲存在 RequestContext
上,這樣後面就能直接通過 from flask import session
來使用它。如果沒有設定 secret_key
變數, open_session
就會返回 None,這個時候會呼叫 make_null_session
來生成一個空的 session,這個特殊的 session 不能進行任何讀寫操作,不然會報異常給使用者。
我們來看看 open_session
方法:
1 2 3 |
def open_session(self, request): return self.session_interface.open_session(self, request) |
在 Flask
中,所有和 session 有關的呼叫,都是轉發到 self.session_interface
的方法呼叫上(這樣使用者就能用自定義的 session_interface
來控制 session 的使用)。而預設的 session_inerface
有預設值:
1 2 |
session_interface = SecureCookieSessionInterface() |
後面遇到 session 有關方法解釋,我們會直接講解 SecureCookieSessionInterface
的程式碼實現,跳過中間的這個轉發說明。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
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 is None: return None # 從 cookie 中獲取 session 變數的值 val = request.cookies.get(app.session_cookie_name) if not 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) except BadSignature: return self.session_class() |
open_session
根據請求中的 cookie 來獲取對應的 session 物件。之所以有 app
引數,是因為根據 app 中的安全設定(比如簽名演算法、secret_key)對 cookie 進行驗證。
這裡有兩點需要特殊說明的:簽名演算法是怎麼工作的?session 物件到底是怎麼定義的?
session 物件
預設的 session 物件是 SecureCookieSession
,這個類就是一個基本的字典,外加一些特殊的屬性,比如 permanent
(flask 外掛會用到這個變數)、modified
(表明例項是否被更新過,如果更新過就要重新計算並設定 cookie,因為計算過程比較貴,所以如果物件沒有被修改,就直接跳過)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class SessionMixin(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 class SecureCookieSession(CallbackDict, SessionMixin): """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 |
怎麼知道例項的資料被更新過呢? SecureCookieSession
是基於 werkzeug/datastructures:CallbackDict
實現的,這個類可以指定一個函式作為 on_update
引數,每次有字典操作的時候(__setitem__
、__delitem__
、clear
、popitem
、update
、pop
、setdefault
)會呼叫這個函式。
NOTE:CallbackDict
的實現很巧妙,但是並不複雜,感興趣的可以自己參考程式碼。主要思路就是過載字典的一些更新操作,讓它們在做原來事情的同時,額外呼叫一下實現儲存的某個函式。
對於開發者來說,可以把 session
簡單地看成字典,所有的操作都是和字典一致的。
簽名演算法
都獲取 cookie 資料的過程中,最核心的幾句話是:
1 2 3 4 5 6 |
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 被篡改了,它會直接放棄使用。
我們繼續看 get_signing_serializer
方法:
1 2 3 4 5 6 7 8 9 10 11 12 |
def get_signing_serializer(self, app): if not app.secret_key: return None signer_kwargs = dict( key_derivation=self.key_derivation, digest_method=self.digest_method ) return URLSafeTimedSerializer(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
)
URLSafeTimedSerializer
是 itsdangerous
庫的類,主要用來進行資料驗證,增加網路中資料的安全性。itsdangerours
提供了多種 Serializer
,可以方便地進行類似 json 處理的資料序列化和反序列的操作。至於具體的實現,因為篇幅限制,就不解釋了。
應答過程
flask 會在請求過來的時候自動解析 cookie 的值,把它變成 session
變數。開發在檢視函式中可以使用它的值,也可以對它進行更新。最後再返回的 response 中,flask 也會自動把 session 寫回到 cookie。我們來看看這部分是怎麼實現的!
之前的文章講解了應答的過程,其中 finalize_response
方法在根據檢視函式的返回生成 response 物件之後,會呼叫 process_response
方法進行處理。process_response
方法的最後有這樣兩句話:
1 2 3 4 5 6 |
def process_response(self, response): ... if not self.session_interface.is_null_session(ctx.session): self.save_session(ctx.session, response) return response |
這裡就是 session 在應答中出現的地方,思路也很簡單,如果需要就呼叫 save_sessoin
,把當前上下文中的 session
物件儲存到 response 。
save_session
的程式碼和 open_session
對應:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
def save_session(self, app, session, response): domain = self.get_cookie_domain(app) path = self.get_cookie_path(app) # 如果 session 變成了空字典,flask 會直接刪除對應的 cookie if not session: if session.modified: response.delete_cookie(app.session_cookie_name, domain=domain, path=path) return # 是否需要設定 cookie。如果 session 發生了變化,就一定要更新 cookie,否則使用者可以 `SESSION_REFRESH_EACH_REQUEST` 變數控制是否要設定 cookie if not self.should_set_cookie(app, session): return httponly = 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, httponly=httponly, domain=domain, path=path, secure=secure) |
這段程式碼也很容易理解,就是從 app
和 session
變數中獲取所有需要的資訊,然後呼叫 response.set_cookie
設定最後的 cookie
。這樣客戶端就能在 cookie 中儲存 session 有關的資訊,以後訪問的時候再次傳送給伺服器端,以此來實現有狀態的互動。
解密 session
有時候在開發或者除錯的過程中,需要了解 cookie 中儲存的到底是什麼值,可以通過手動解析它的值。session
在 cookie
中的值,是一個字串,由句號分割成三個部分。第一部分是 base64
加密的資料,第二部分是時間戳,第三部分是校驗資訊。
前面兩部分的內容可以通過下面的方式獲取,程式碼也可直觀,就不給出解釋了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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
這個三方的外掛,方便我們的開發
參考資料
請使用手機”掃一掃”x