Flask-Cookies和Session

cookies

在Flask的框架中,本身已經封裝了 cookie的respons,request 有存儲就有讀取及刪除,那麼就拿購物車來舉例算法

  在咱們登錄的時候會有以前在購物車存放的物品。也就是說在一個地方爲咱們保存了這些數據。前提有一個是要你登錄以後才能看到本身的購物車數據庫

  cookie對應的是client session對應的是server。 也就是說,要在服務器上登陸你對應的帳戶,才能看到你本身在購物車添加的物品。可是django

  物品那麼多,不能都存在服務器上吧,因此通常cookie都存在本身的計算機上,只是找不到而已,這裏就不說了。一些簡單的原理實現一下。json

首先來看cookie的簡單存儲,讀數據,刪除數據怎麼實現flask

# cookie相關的操做,依賴於make_response庫,調用cookie依賴request
from flask import Flask, make_response, request


app = Flask(__name__)

app.config.from_pyfile('config.ini')

# cookie存在client裏。存在本身電腦的我的當資料裏
# 存cookie的方法
@app.route('/setcookie')
def set_cookie():
    resp = make_response('存儲cookie')
    # 使用set方法,來存儲 key-values 形式的數據
    resp.set_cookie('productname','衛生紙')
    return resp


# 獲取,取到數據,調用cookie的方法
@app.route('/getcookie')
def get_cookie():
    # 經過request模塊的cookies屬性的方法指定Key 來調用value
    resp = request.cookies.get('productname')
    return resp

# 刪除cookie的方法
@app.route('/delcookie')
def del_cookie():
    # 經過make_response對象內置的delete_cookie方法來指定key來刪除vaule
    resp = make_response('刪除cookie')
    resp.delete_cookie('productname')
    return resp



if __name__ == "__main__":
    app.run()

session

  • session:存放在客戶端的鍵值對
  • token:存放在客戶端,經過算法來校驗

在使用session以前必須如今設置一下密鑰瀏覽器

app.secret_key="asdas" #值隨便

除請求對象以外,還有一個 session 對象。它容許你在不一樣請求間存儲特定用戶的信息。它是在 Cookies 的基礎上實現的,而且對 Cookies 進行密鑰簽名要使用會話,你須要設置一個密鑰。 (app.session_interface對象)安全

設置:session['username'] = 'xxx'
# 在django中發什麼三件事,
#    1 生成一個隨機的字符串 
#    2 往數據庫存 
#    3 寫入cookie返回瀏覽器

# 在flask中他沒有數據庫,但session是怎樣實現的?
# 生成一個密鑰寫入這個cookie,而後下次請求的時候,經過這個cookie解密,而後賦值給session
#咱們經過app.session_interface來查看
  
刪除:session.pop('username', None)

save_session的參數

save_sessionapp.session_interface中的函數,他的參數以下:服務器

參數 做用
key
value
max_age=None 超時時間 cookie須要延續的時間(以秒爲單位)若是參數是 None`,這個cookie會延續到瀏覽器關閉爲止| |expires=None|超時時間(IE requires expires, so set it if hasn't been already.)| |path='/'|Cookie生效的路徑,/ 表示根路徑,特殊的:根路徑的cookie能夠被任何url的頁面訪問,瀏覽器只會把cookie回傳給帶有該路徑的頁面,這樣能夠避免將cookie傳給站點中的其餘的應用。| |domain=None|Cookie生效的域名 你可用這個參數來構造一個跨站cookie。如,domain=".example.com"所構造的cookie對下面這些站點都是可讀的:<br />www.example.comwww2.example.com an.other.sub.domain.example.com `。若是該參數設置爲 None ,cookie只能由設置它的站點讀取
secure=False 瀏覽器將經過HTTPS來回傳cookie
httponly=False 只能http協議傳輸,沒法被JavaScript獲取(不是絕對,底層抓包能夠獲取到也能夠被覆蓋)

session源碼執行流程

請求第一次過來時

在flask請求上下文和應用上下文中已經知道session是一個LocalProxy()對象:cookie

current_app = LocalProxy(_find_app)
request = LocalProxy(partial(_lookup_req_object, 'request'))
session = LocalProxy(partial(_lookup_req_object, 'session'))
g = LocalProxy(partial(_lookup_app_object, 'g'))

客戶端的請求進來時,會調用app.wsgi_app():

def wsgi_app(self, environ, start_response):
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            ctx.push()
            # 尋找視圖函數,並執行
            # 獲取返回值 response
            response = self.full_dispatch_request()

此時,會生成一個ctx,其本質是一個RequestContext對象:

class RequestContext(object):
    def __init__(self, app, environ, request=None, session=None):
        self.app = app
        if request is None:
            request = app.request_class(environ)
        self.request = request
        self.url_adapter = None
        try:
            self.url_adapter = app.create_url_adapter(self.request)
        except HTTPException as e:
            self.request.routing_exception = e
        self.flashes = None
        self.session = session

RequestContext對象中定義了session,且初值爲None。

接着繼續看wsgi_app函數中,ctx.push()函數:

def push(self):
    app_ctx = _app_ctx_stack.top
    if app_ctx is None or app_ctx.app != self.app:
        app_ctx = self.app.app_context()
        app_ctx.push()
        self._implicit_app_ctx_stack.append(app_ctx)
    else:
        self._implicit_app_ctx_stack.append(None)

    if hasattr(sys, 'exc_clear'):
        sys.exc_clear()

    # 把ctx對象加入到Local()對象中
    _request_ctx_stack.push(self)

    if self.session is None:
        session_interface = self.app.session_interface
        self.session = session_interface.open_session(
            self.app, self.request
        )

        if self.session is None:
            self.session = session_interface.make_null_session(self.app)

主要看後半部分代碼。判斷session是否爲空,我在RequestContext中看到session初值爲空.

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

session_interface = SecureCookieSessionInterface()

執行SecureCookieSessionInterface.open_session()來生成默認session對象:

def open_session(self, app, request):
    # 獲取session簽名的算法
    s = self.get_signing_serializer(app)
    # 若是爲空 直接返回None
    if s is None:
        return None
    val = request.cookies.get(app.session_cookie_name)
    # 若是val爲空,即request.cookies爲空
    if not val:
        return self.session_class()
    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()

請求第一次來時,request.cookies爲空,即返回self.session_class():

session_class = SecureCookieSession

SecureCookieSession

class SecureCookieSession(CallbackDict, SessionMixin):
    modified = False
    accessed = False
    
    def __init__(self, initial=None):
        def on_update(self):
            self.modified = True
            self.accessed = True

        super(SecureCookieSession, self).__init__(initial, on_update)

    def __getitem__(self, key):
        self.accessed = True
        return super(SecureCookieSession, self).__getitem__(key)

    def get(self, key, default=None):
        self.accessed = True
        return super(SecureCookieSession, self).get(key, default)

    def setdefault(self, key, default=None):
        self.accessed = True
        return super(SecureCookieSession, self).setdefault(key, default)

看其繼承關係,其實就是一個特殊的字典。到此咱們知道了session就是一個特殊的字典,調用SecureCookieSessionInterface類的open_session()建立,並保存在ctx中,即RequestContext對象中。但最終由session = LocalProxy(..., 'session')對象代爲管理,到此,在視圖函數中就能夠導入session並使用了。

請求第二次進來

當請求第二次到來時,與第一次的不一樣就在open_session()那個val判斷處,此時cookies不爲空, 獲取cookie的有效時長,若是cookie依然有效,經過與寫入時一樣的簽名算法將cookie中的值解密出來並寫入字典並返回中,若cookie已經失效,則仍然返回空字典

SecureCookieSession

默認的 session 對象是 SecureCookieSession,這個類就是一個基本的字典,外加一些特殊的屬性,好比 permanent(flask 插件會用到這個變量)、modified(代表實例是否被更新過,若是更新過就要從新計算並設置 cookie,由於計算過程比較貴,因此若是對象沒有被修改,就直接跳過)。

怎麼知道實例的數據被更新過呢?SecureCookieSession是基於 werkzeug/datastructures:CallbackDict實現的,這個類能夠指定一個函數做爲 on_update 參數,每次有字典操做的時候(__setitem__、__delitem__、clear、popitem、update、pop、setdefault)會調用這個函數。

SecureCookieSession:

class SecureCookieSession(CallbackDict, SessionMixin):
 
    modified = False
    accessed = False
 
    def __init__(self, initial=None):
        def on_update(self):  
            self.modified = True
            self.accessed = True
        #將on_update()傳遞給CallbackDict
        super(SecureCookieSession, self).__init__(initial, on_update)
 
    def __getitem__(self, key):
        self.accessed = True
        return super(SecureCookieSession, self).__getitem__(key)
 
    def get(self, key, default=None):
        self.accessed = True
        return super(SecureCookieSession, self).get(key, default)
 
    def setdefault(self, key, default=None):
        self.accessed = True
        return super(SecureCookieSession, self).setdefault(key, default)

繼承的 CallbackDict:

class CallbackDict(UpdateDictMixin, dict):
 
    def __init__(self, initial=None, on_update=None):
        dict.__init__(self, initial or ())
        self.on_update = on_update
 
    def __repr__(self):
        return '<%s %s>' % (
            self.__class__.__name__,
            dict.__repr__(self)
        )

CallbackDict又繼承UpdateDictMixin:

class UpdateDictMixin(object):
    on_update = None
 
    def calls_update(name):
        def oncall(self, *args, **kw):
            rv = getattr(super(UpdateDictMixin, self), name)(*args, **kw)
            if self.on_update is not None:
                self.on_update(self)
            return rv
        oncall.__name__ = name
        return oncall
 
    def setdefault(self, key, default=None):
        modified = key not in self
        rv = super(UpdateDictMixin, self).setdefault(key, default)
        if modified and self.on_update is not None:
            self.on_update(self)
        return rv
 
    def pop(self, key, default=_missing):
        modified = key in self
        if default is _missing:
            rv = super(UpdateDictMixin, self).pop(key)
        else:
            rv = super(UpdateDictMixin, self).pop(key, default)
        if modified and self.on_update is not None:
            self.on_update(self)
        return rv
 
    __setitem__ = calls_update('__setitem__')
    __delitem__ = calls_update('__delitem__')
    clear = calls_update('clear')
    popitem = calls_update('popitem')
    update = calls_update('update')
    del calls_update

UpdateDictMixin()可知,對session進行改動會調用pop, __setitem__等方法,同時就會調用on_update()方法,從而修改modify,security的值

簽名算法

都獲取 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 處理的數據序列化和反序列的操做。

session的生命週期

前面的幾個問題實際上都發生在wsgi_app()前兩句函數中,主要就是ctx.push()函數中,下面看看wsgi_app()後面幹了嘛:

def wsgi_app(self, environ, start_response):
  
    ctx = self.request_context(environ)
    error = None
    try:
        try:
            # ctx.push函數是前半部分最重要的一個函數
            # 生成request和session並將兩者保存到RequestContext()對象ctxz中
            # 最後將ctx,push到LocalStack()對象_request_ctx_stack中
            ctx.push()
            # 尋找視圖函數,並執行
            response = self.full_dispatch_request()
        except Exception as e:
            error = e
            response = self.handle_exception(e)
        except:
            error = sys.exc_info()[1]
            raise
        return response(environ, start_response)
    finally:
        if self.should_ignore_error(error):
            error = None
        # 最後, 將本身請求在local中的數據清除
        ctx.auto_pop(error)

full_dispatch_request

def full_dispatch_request(self):
    #執行before_first_request
    self.try_trigger_before_first_request_functions()
    try:
        # 觸發request_started 信號
        request_started.send(self)
        # 調用before_request
        rv = self.preprocess_request()
        if rv is None:
            #執行視圖函數
            rv = self.dispatch_request()
    except Exception as e:
        rv = self.handle_user_exception(e)
    return self.finalize_request(rv)

前半部分就在執行flask鉤子,before_first_request, before_request以及信號,接着執行視圖函數生成rv,

咱們主要看finalize_request(rv)

def finalize_request(self, rv, from_error_handler=False):
        response = self.make_response(rv)
        try:
            response = self.process_response(response)
            request_finished.send(self, response=response)
        except Exception:
            if not from_error_handler:
                raise
            self.logger.exception('Request finalizing failed with an '
                                  'error while handling an error')
        return response

首先根據rv生成response。再執行process_response:

def process_response(self, response):
    ctx = _request_ctx_stack.top
    bp = ctx.request.blueprint
    funcs = ctx._after_request_functions
    
    if bp is not None and bp in self.after_request_funcs:
        funcs = chain(funcs, reversed(self.after_request_funcs[bp]))
        
    if None in self.after_request_funcs:
        funcs = chain(funcs, reversed(self.after_request_funcs[None]))
        for handler in funcs:
            response = handler(response)
            
    if not self.session_interface.is_null_session(ctx.session):
        self.session_interface.save_session(self, ctx.session, response)
        
    return response

前半部分主要執行flask的鉤子,看後面,判斷,session是否爲空,若是不爲空,則執行save_session():

def save_session(self, app, session, response):
    domain = self.get_cookie_domain(app)
    path = self.get_cookie_path(app)
 
    # If the session is modified to be empty, remove the cookie.
    # If the session is empty, return without setting the cookie.
    if not session:
        if session.modified:
            response.delete_cookie(
                app.session_cookie_name,
                domain=domain,
                path=path
            )
 
        return
 
    # Add a "Vary: Cookie" header if the session was accessed at all.
    if session.accessed:
        response.vary.add('Cookie')
 
    if not self.should_set_cookie(app, session):
        return
 
    httponly = self.get_cookie_httponly(app)
    secure = self.get_cookie_secure(app)
    samesite = self.get_cookie_samesite(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,
        samesite=samesite
    )

save_session()比較簡單,且有註釋,便再也不講解,主要就是將session寫入response.set_cookie中。這樣便完成session的寫入response工做,並由response返回至客戶端。

再請求結束時會執行wsgi_app()finally:ctx.auto_pop(error)函數,將與對應請求相關的request,session清除,session生命週期便結束。

總結

至此,flask內置session的機制便講解完畢,session的實現是依賴與flask的上下文管理,所以先弄清楚flask上下文,再來看session就比較容易理解。其主要的就是SecureCookieSessionInterface對象的open_session()save_session()open_session在請求剛進來時執行,完成session對象的建立(就是一特殊字典),在視圖函數中完成對session的賦值操做,save_session()在視圖函數執行完後,生成response後執行,將session寫入response的cookie中。

相關文章
相關標籤/搜索