將 flask 中的 session 存儲到 SQLite 數據庫中


將 flask 中的 session 存儲到 SQLite 數據庫中

使用 flask 構建服務器后端時,常需要在瀏覽器端存儲 cookie 用於識別不同用戶,根據不同的 cookie 判斷出當前請求訪問的用戶。而在服務器端 flask 提供了易用的 session 代理,通過 from flask import session, 可以引入 session 並將其作為特定用戶信息的字典來用。

要使用 session 代理,首先要給 flask 實例設置 secret_key,然后就可以在請求上下文中使用 session(如在特定的路由中):

from flask import Flask, session, render_template
app = Flask( __name__ )
app.secret_key = hashlib.sha1( 'abcdefg'.encode() ).hexdigest()
# app.config[ 'SECRET_KEY' ] = hashlib.sha1( 'abcdefg'.encode() ).hexdigest()  # same with previous one statement

@app.route( '/', methods = [ 'GET' ])
def index():
    session[ 'user' ] = 'test'
    return render_template( 'index.html' )

由於 flask 默認的 session 實現方式是 SecureCookieSessionInterface,它將 session 中的數據以加密方式存儲在客戶端的 cookie 中,因此在使用 session 前,需要首先設置 secret_key,這樣才能避免客戶端中的 cookie 泄漏信息。

將 session 信息存儲在數據庫中

盡管 flask 默認的 session 數據是加密之后存儲在客戶端的 cookie 中,但如果 session 要存儲大量敏感的信息,還是將其存儲在服務端較好。flask 已經提供了 SessionInterface 接口,要自己實現 session 的存儲方式,需要實現該接口並將 flask 實例 app 的 session_interface 指向其實現類的實例。此外還要實現自定義的 Session 類,在自定義的 SessionInterface 實現類中使用自定義的 Session 類。

自定義 Session 類

自定義的 Session 類實現起來很簡單,因為它的工作方式與 dict 很類似,可以直接將其繼承 dict,這里使用 werkzeug 中提供的 CallbackDict,另外還需要繼承的父類是 SessionMixin,以便在 flask 中使用該 Session。由於要使用的是 sqlite 存儲方式,這里我將類命名為 DbSession:

# file: dbsessions.py
from flask.sessions import SessionMixin, SessionInterface
from werkzeug import CallbackDict

class DbSession( CallbackDict, SessionMixin ):
    def __init__( self, initial = None, sid = None, new = False ):
        def on_update( self ):
            self.modified = True
        
        CallbackDict.__init__( self, initial, on_update )
        self.sid = sid
        self.new = new
        self.modified = False

Server-side sessions with SQLite 這篇示例中,由於實現的其實現的 SqliteSession 繼承的是 MutableMapping,並且沒有使用 flask-sqlalchemy,因此需要有 __getitem____setitem__ 等實現數據庫的操作,並且需要定義查詢的 SQL 語句。而我們使用 flask-sqlalchemy 處理底層的數據庫事務,因此 Session 實現類較為簡單。

定義 Session 數據模型

為了將 session 數據存儲在 sqlite 數據庫中,我們還需要定義它的 Model,flask-sqlalchemy 已經為我們處理好了底層繁瑣的代碼,在應用中直接使用 flask-sqlalchemy 提供的數據庫實例即可。

使用 flask-sqlalchemy:

# file: myglobal.py
from flask import Flask
from flask_sqlalchemy import SQLAlchemy

app = Flask( __name__ )
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///database.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True
db = SQLAlchemy( app )
# file: dbsessions.py
from .myglobal import db
from datetime import datetime, timedelta

class Sessions( db.Model ):
    __tablename__ = 'sessions'
    sid = db.Column( db.String( 255 ), primary_key = True )
    expired = db.Column( db.DateTime )
    session = db.Column( db.Text ) # it refers session content

    def __init__( self, **kwargs ):
        super( Sessions, self ).__init__( **kwargs )
        if 'sid' in kwargs:
            self.sid = kwargs[ 'sid' ]
        if 'expired' in kwargs:
            self.expired = kwargs[ 'expired' ]
        if 'session' in kwargs:
            self.session = kwargs[ 'session' ]
    
    def __repr__( self ):
        return '<Session %s>' % self.session

這里的 sid 字段為用於識別用戶身份的標識,expired 字段用於設定過期時間,session 字段是實際存儲的 session 內容,由於 session 的數據結構與 dict 一樣,因此可以考慮使用 json 格式序列化到數據庫中。

順便提一下,如果在程序中使用 session,並要以 json 格式序列化 session,那么 session 中最好不要用到 Datetime 類,否則在運行過程中會提示 Datetime 無法被 JSON 序列化 之類的錯誤,使用時間戳替代即可:

# in flask app context
session[ 'login' ] = datetime.now()  # 當用 JSON 格式序列化 session 時,會提示 Datetime 對象無法序列化
session[ 'login' ] = datetime.now().timestamp() # 正常

實現 SessionInterface 接口

接下來要實現 flask 中提供的 SessionInterface 接口,較前面兩部分內容相對復雜,但是其實原理也很簡單。這里需要在實現類中完成 sid 的分配和客戶端 cookie 的讀寫,sid 分配需要盡可能隨機,可以直接使用 python 中的 uuid 模塊。序列化 session 時使用 JSON 格式,因此需要引入 json 模塊(也可以使用 pickle 模塊進行序列化)。

SessionInterface 的兩個主要接口方法是 open_sessionsave_session

# file: dbsessions.py
from uuid import uuid4
import json

class DatabaseSessionInterface( SessionInterface ):
    session_class = DbSession
    serializer = json  # or pickle

    def __init__( self, db = db, prefix = 'session:' ):
        self.db = db
        self.prefix = prefix

    def generate_sid( self ):
        return str( uuid4() )

    def get_expiration_time( self, app, session = None ):
        """
        可以直接使用父類的默認超時時間方法,為了自定義默認超時時間進行重寫
        """
        if session.permanent:
            return datetime.now() + app.permanent_session_lifetime

        return datetime.now() + timedelta( minutes = 10 )

    def open_session( self, app, request ):
        sid = request.cookies.get( app.session_cookie_name )  # 嘗試從請求中獲取存儲的 cookie(與 session 相關的 cookie)
        if not sid: 
            # 沒有從 cookie 中找到相關 sid,說明客戶端是第一次訪問,需要構造 sid
            sid = self.generate_sid()
            # create new Session Model instance
            dbrecord = Sessions( sid = sid )
            sc = self.session_class( sid = sid, new = True )
            return sc
        # if client have a session_cookie, take relative one from database
        dbrecord = Sessions.query.filter_by( sid = sid ).first()
        if dbrecord is None:
            return self.session_class( sid = sid ) # in case of wrong sid in session_cookie
        else:
            return self.session_class( self.serializer.loads( dbrecord.session ), sid = sid )
    
    def save_session( self, app, session, response ):
        domain = self.get_cookie_domain( app )

        dbrecord = Sessions.query.filter_by( sid = session.sid ).first()
        if not session:
            # 內容為空並且被修改過,說明 session 已經被刪除
            if session.modified:
                db.session.delete( dbrecord )
                db.session.commit()
                response.delete_cookie( app.session_cookie_name, domain = domain )
            
            return

        if dbrecord is None:
            dbrecord = Sessions( sid = session.sid )
            db.session.add( dbrecord )
        # update database record
        dbrecord.session = self.serializer.dumps( session )
        sexp = self.get_expiration_time( app, session )
        dbrecord.expired = sexp
        db.session.commit()
        response.set_cookie( app.session_cookie_name, session.sid, expires = sexp, httponly = True, domain = domain )

實現了 open_session 和 save_session 方法的實現類就可以在 flask 應用中使用了:

app.session_interface = DatabaseSessionInterface()

這里有必要說一下處理 session 的流程:

  1. 若客戶端第一次請求服務器(服務器使用了 session,但服務器上沒有相應的 session),首先調用 open_session 方法,返回一個新創建的 DbSession 對象。在 open_session 方法中最終一定需要返回一個字典結構的對象,我們這里也就是 DbSession 實例。

  2. 接下來 flask 會將 open_session 方法中返回的 DbSession 對象的 sid 寫入到客戶端的 cookie 響應中,並將該對象作為 session 參數調用 save_session 方法。根據 session 中的內容(當 session 中的內容為空時,not session 為 True)和屬性執行刪除或更新 session 的操作

  3. 客戶端存儲了有 sid 的 cookie 后,后續請求到來時,在 open_session 方法將會返回在數據庫中存儲的 session 條目。這樣就達到了識別用戶的目的。

總結

總的來說實現自定義的 session 存儲方式並不困難,在參考了 stackoverflow 上相關問題的解答和一些示例后再根據自己的需求進行實現和改進。其實 Server-side sessions with SQLite 這篇示例已經很接近於我的需求了,但是我的具體代碼中和該示例有所區別。

比如,示例中為每個 session 創建一個單獨的 sqlite 數據庫文件,而我的代碼中將 session 作為表的一行。另外,示例中會話結束時就會刪除對應的數據庫文件,而我修改了默認的超時時間方法,因此需要在會話結束后並超時時刪除對應的數據庫條目,但是目前的代碼中並沒有相應的實現。不過基本上來說已經達到了最初的目的。

envrionments or requirements

  • python3.6
  • flask
  • flask-sqlalchemy

References

  1. https://stackoverflow.com/questions/17694469/flask-save-session-data-in-database-like-using-cookies
  2. http://flask.pocoo.org/snippets/75/
  3. Server-side sessions with SQLite
  4. flask.sessions.SessionInterface


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM