flask 源碼解析:session

7.flask 源碼解析:session

session 簡介

在解析 session 的實現(xiàn)之前,我們先介紹一下 session 怎么使用。session 可以看做是在不同的請求之間保存數(shù)據(jù)的方法,因為 HTTP 是無狀態(tài)的協(xié)議,但是在業(yè)務(wù)應(yīng)用上我們希望知道不同請求是否是同一個人發(fā)起的。比如購物網(wǎng)站在用戶點擊進入購物車的時候,服務(wù)器需要知道是哪個用戶執(zhí)行了這個操作。

在 flask 中使用 session 也很簡單,只要使用 from flask import session 導入這個變量,在代碼中就能直接通過讀寫它和 session 交互。

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, stranger\n'


@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來執(zhí)行的,使用 curl 命令也能達到相同的效果):

直接訪問的話,我們可以看到返回 hello stranger

?  ~ 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 是告訴服務(wù)器這是表單數(shù)據(jù),--session=mysession 是把請求的 cookie 等信息保存到這個變量中,后面可以通過變量來指定 session:

?  ~ 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,值是一堆看起來隨機的字符串。

繼續(xù),這個時候我們用 --session=mysession 參數(shù)把這次的請求帶上保存在 mysession 中的信息,登陸后訪問,可以看到登陸的用戶名:

?  ~ 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

這次注意在發(fā)送的請求中,客戶端帶了 Cookie 頭部,上面的值保存了前一個請求的 response 給我們設(shè)置的值。

總結(jié)一下:session 是通過在客戶端設(shè)置 cookie 實現(xiàn)的,每次客戶端發(fā)送請求的時候會附帶著所有的 cookie,而里面保存著一些重要的信息(比如這里的用戶信息),這樣服務(wù)器端就能知道客戶端的信息,然后根據(jù)這些數(shù)據(jù)做出對應(yīng)的判斷,就好像不同請求之間是有記憶的。

解析

我們知道 session 是怎么回事了,這部分就分析一下 flask 是怎么實現(xiàn)它的。

請求過程

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

  • 請求過來的時候,flask 會根據(jù) cookie 信息創(chuàng)建出 session 變量(如果 cookie 不存在,這個變量有可能為空),保存在該請求的上下文中
  • 視圖函數(shù)可以獲取 session 中的信息,實現(xiàn)自己的邏輯處理
  • flask 會在發(fā)送 response 的時候,根據(jù) session 的值,把它寫回到 cookie 中

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

flask 上下文那篇文章中,我們知道,每次請求過來的時候,我們訪問的 requestsession 變量都是 RequestContext 實例的變量。在 RequestContext.Push() 方法的最后有這么一段代碼:

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 來使用它。如果沒有設(shè)置 secret_key 變量, open_session 就會返回 None,這個時候會調(diào)用 make_null_session 來生成一個空的 session,這個特殊的 session 不能進行任何讀寫操作,不然會報異常給用戶。

我們來看看 open_session 方法:

def open_session(self, request):
    return self.session_interface.open_session(self, request)

Flask 中,所有和 session 有關(guān)的調(diào)用,都是轉(zhuǎn)發(fā)到 self.session_interface 的方法調(diào)用上(這樣用戶就能用自定義的 session_interface 來控制 session 的使用)。而默認的 session_inerface 有默認值:

session_interface = SecureCookieSessionInterface()

后面遇到 session 有關(guān)方法解釋,我們會直接講解 SecureCookieSessionInterface 的代碼實現(xiàn),跳過中間的這個轉(zhuǎn)發(fā)說明。

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 的數(shù)據(jù)需要驗證是否有篡改,所以需要簽名算法來讀取里面的值
    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 根據(jù)請求中的 cookie 來獲取對應(yīng)的 session 對象。之所以有 app 參數(shù),是因為根據(jù) app 中的安全設(shè)置(比如簽名算法、secret_key)對 cookie 進行驗證。

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

session 對象

默認的 session 對象是 SecureCookieSession,這個類就是一個基本的字典,外加一些特殊的屬性,比如 permanent(flask 插件會用到這個變量)、modified(表明實例是否被更新過,如果更新過就要重新計算并設(shè)置 cookie,因為計算過程比較貴,所以如果對象沒有被修改,就直接跳過)。

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

怎么知道實例的數(shù)據(jù)被更新過呢? SecureCookieSession 是基于 werkzeug/datastructures:CallbackDict 實現(xiàn)的,這個類可以指定一個函數(shù)作為 on_update 參數(shù),每次有字典操作的時候(__setitem____delitem__、clear、popitem、update、popsetdefault)會調(diào)用這個函數(shù)。

NOTECallbackDict 的實現(xiàn)很巧妙,但是并不復雜,感興趣的可以自己參考代碼。主要思路就是重載字典的一些更新操作,讓它們在做原來事情的同時,額外調(diào)用一下實現(xiàn)保存的某個函數(shù)。

對于開發(fā)者來說,可以把 session 簡單地看成字典,所有的操作都是和字典一致的。

簽名算法

都獲取 cookie 數(shù)據(jù)的過程中,最核心的幾句話是:

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 有關(guān),signing_serializer 保證了 cookie 和 session 的轉(zhuǎn)換過程中的安全問題。如果 flask 發(fā)現(xiàn)請求的 cookie 被篡改了,它會直接放棄使用。

我們繼續(xù)看 get_signing_serializer 方法:

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)

我們看到這里需要用到很多參數(shù):

  • secret_key:密鑰。這個是必須的,如果沒有配置 secret_key 就直接使用 session會報錯
  • salt:為了增強安全性而設(shè)置一個 salt 字符串(可以自行搜索“安全加鹽”了解對應(yīng)的原理)
  • serializer:序列算法
  • signer_kwargs:其他參數(shù),包括摘要/hash算法(默認是 sha1)和 簽名算法(默認是 hmac

URLSafeTimedSerializeritsdangerous 庫的類,主要用來進行數(shù)據(jù)驗證,增加網(wǎng)絡(luò)中數(shù)據(jù)的安全性。itsdangerours 提供了多種 Serializer,可以方便地進行類似 json 處理的數(shù)據(jù)序列化和反序列的操作。至于具體的實現(xiàn),因為篇幅限制,就不解釋了。

應(yīng)答過程

flask 會在請求過來的時候自動解析 cookie 的值,把它變成 session 變量。開發(fā)在視圖函數(shù)中可以使用它的值,也可以對它進行更新。最后再返回的 response 中,flask 也會自動把 session 寫回到 cookie。我們來看看這部分是怎么實現(xiàn)的!

之前的文章講解了應(yīng)答的過程,其中 finalize_response 方法在根據(jù)視圖函數(shù)的返回生成 response 對象之后,會調(diào)用 process_response 方法進行處理。process_response 方法的最后有這樣兩句話:

def process_response(self, response):
    ...
    if not self.session_interface.is_null_session(ctx.session):
        self.save_session(ctx.session, response)
    return response

這里就是 session 在應(yīng)答中出現(xiàn)的地方,思路也很簡單,如果需要就調(diào)用 save_sessoin,把當前上下文中的 session 對象保存到 response 。

save_session 的代碼和 open_session 對應(yīng):

def save_session(self, app, session, response):
        domain = self.get_cookie_domain(app)
        path = self.get_cookie_path(app)

        # 如果 session 變成了空字典,flask 會直接刪除對應(yīng)的 cookie
        if not session:
            if session.modified:
                response.delete_cookie(app.session_cookie_name,
                                       domain=domain, path=path)
            return

        # 是否需要設(shè)置 cookie。如果 session 發(fā)生了變化,就一定要更新 cookie,否則用戶可以 `SESSION_REFRESH_EACH_REQUEST` 變量控制是否要設(shè)置 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)

這段代碼也很容易理解,就是從 appsession 變量中獲取所有需要的信息,然后調(diào)用 response.set_cookie 設(shè)置最后的 cookie。這樣客戶端就能在 cookie 中保存 session 有關(guān)的信息,以后訪問的時候再次發(fā)送給服務(wù)器端,以此來實現(xiàn)有狀態(tài)的交互。

解密 session

有時候在開發(fā)或者調(diào)試的過程中,需要了解 cookie 中保存的到底是什么值,可以通過手動解析它的值。sessioncookie 中的值,是一個字符串,由句號分割成三個部分。第一部分是 base64 加密的數(shù)據(jù),第二部分是時間戳,第三部分是校驗信息。

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

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'

總結(jié)

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

session 主要是用來在不同的請求之間保存信息,最常見的應(yīng)用就是登陸功能。雖然直接通過 session 自己也可以寫出來不錯的登陸功能,但是在實際的項目中可以考慮 flask-login 這個三方的插件,方便我們的開發(fā)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內(nèi)容

  • 會話(Session)跟蹤是Web程序中常用的技術(shù),用來跟蹤用戶的整個會話。常用的會話跟蹤技術(shù)是Cookie與Se...
    chinariver閱讀 5,650評論 1 49
  • ? ??在第1章,我們已經(jīng)了解了Flask的基本知識,如果想要進一步開發(fā)更復雜的Flask應(yīng)用,我們就得了解F...
    懵懂_傻孩紙閱讀 2,987評論 0 4
  • 背景在HTTP協(xié)議的定義中,采用了一種機制來記錄客戶端和服務(wù)器端交互的信息,這種機制被稱為cookie,cooki...
    時芥藍閱讀 2,380評論 1 17
  • [TOC]一直想做源碼閱讀這件事,總感覺難度太高時間太少,可望不可見。最近正好時間充裕,決定試試做一下,并記錄一下...
    何柯君閱讀 7,210評論 3 98
  • 1、為什么需要繼承機制? 由于非compile范圍的依賴信息是不能在“依賴鏈”中傳遞的,所以有需要的工程只能單獨配...
    黑猴子的家閱讀 135評論 0 1