flask 源碼解析:session

這是 flask 源碼解析系列文章的其中一篇,本系列全部文章列表:html

session 簡介

在解析 session 的實現以前,咱們先介紹一下 session 怎麼使用。session 能夠看作是在不一樣的請求之間保存數據的方法,由於 HTTP 是無狀態的協議,可是在業務應用上咱們但願知道不一樣請求是不是同一我的發起的。好比購物網站在用戶點擊進入購物車的時候,服務器須要知道是哪一個用戶執行了這個操做。安全

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

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 來執行的,使用 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 是告訴服務器這是表單數據,--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,值是一堆看起來隨機的字符串。

繼續,這個時候咱們用 --session=mysession 參數把此次的請求帶上保存在 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

此次注意在發送的請求中,客戶端帶了 Cookie 頭部,上面的值保存了前一個請求的 response 給咱們設置的值。

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

解析

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

請求過程

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

  • 請求過來的時候,flask 會根據 cookie 信息建立出 session 變量(若是 cookie 不存在,這個變量有可能爲空),保存在該請求的上下文中

  • 視圖函數能夠獲取 session 中的信息,實現本身的邏輯處理

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

注意:session 和 cookie 的轉化過程當中,應該考慮到安全性,否則直接使用僞造的 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 來使用它。若是沒有設置 secret_key 變量, open_session 就會返回 None,這個時候會調用 make_null_session 來生成一個空的 session,這個特殊的 session 不能進行任何讀寫操做,否則會報異常給用戶。

咱們來看看 open_session 方法:

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

Flask 中,全部和 session 有關的調用,都是轉發到 self.session_interface 的方法調用上(這樣用戶就能用自定義的 session_interface 來控制 session 的使用)。而默認的 session_inerface 有默認值:

session_interface = SecureCookieSessionInterface()

後面遇到 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 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,由於計算過程比較貴,因此若是對象沒有被修改,就直接跳過)。

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__clearpopitemupdatepopsetdefault)會調用這個函數。

NOTECallbackDict 的實現很巧妙,可是並不複雜,感興趣的能夠本身參考代碼。主要思路就是重載字典的一些更新操做,讓它們在作原來事情的同時,額外調用一下實現保存的某個函數。

對於開發者來講,能夠把 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 被篡改了,它會直接放棄使用。

咱們繼續看 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)

咱們看到這裏須要用到不少參數:

  • 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 方法進行處理。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 在應答中出現的地方,思路也很簡單,若是須要就調用 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
        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)

這段代碼也很容易理解,就是從 appsession 變量中獲取全部須要的信息,而後調用 response.set_cookie 設置最後的 cookie。這樣客戶端就能在 cookie 中保存 session 有關的信息,之後訪問的時候再次發送給服務器端,以此來實現有狀態的交互。

解密 session

有時候在開發或者調試的過程當中,須要瞭解 cookie 中保存的究竟是什麼值,能夠經過手動解析它的值。sessioncookie 中的值,是一個字符串,由句號分割成三個部分。第一部分是 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 這個三方的插件,方便咱們的開發

參考資料

相關文章
相關標籤/搜索