Django、Flask、およびRedisチュートリアル:Pythonフレームワーク間のWebアプリケーションセッション管理
公開: 2022-03-11DjangoとFlask:Djangoが間違った選択である場合
私は多くの個人プロジェクトやクライアントプロジェクトでDjangoを愛し、使用しています。主に、より古典的なWebアプリケーションやリレーショナルデータベースを使用するプロジェクトです。 ただし、Djangoは特効薬ではありません。
設計上、DjangoはORM、テンプレートエンジンシステム、および設定オブジェクトと非常に緊密に結合されています。 さらに、これは新しいプロジェクトではありません。下位互換性を維持するために多くの荷物を運びます。
一部のPython開発者は、これを大きな問題と見なしています。 彼らは、Djangoは十分な柔軟性がなく、可能であればそれを避け、代わりにFlaskのようなPythonマイクロフレームワークを使用すると言います。
私はその意見を共有しません。 Djangoは、すべてのプロジェクト仕様に適合していなくても、適切な場所と時間で使用すると優れています。 マントラが行くように:「仕事に適切なツールを使用してください」。
(適切な場所と時間ではない場合でも、Djangoを使用したプログラミングには独自の利点がある場合があります。)
場合によっては、より軽量なフレームワーク(Flaskなど)を使用すると便利な場合があります。 多くの場合、これらのマイクロフレームワークは、ハッキングがいかに簡単であるかを理解したときに輝き始めます。
救助へのマイクロフレームワーク
私のクライアントプロジェクトのいくつかでは、Djangoをあきらめてマイクロフレームワークに移行することについて説明しました。通常、クライアントが興味深いこと(たとえば、アプリケーションオブジェクトにZeroMQを埋め込む)とプロジェクトを実行したい場合です。 Djangoで目標を達成するのはもっと難しいようです。
より一般的には、Flaskは次の場合に役立ちます。
- シンプルなRESTAPIバックエンド
- データベースアクセスを必要としないアプリケーション
- NoSQLベースのWebアプリ
- カスタムURL構成など、非常に特定の要件を持つWebアプリ
同時に、私たちのアプリには、Djangoが数年前に解決したユーザー登録やその他の一般的なタスクが必要でした。 軽量であるため、Flaskには同じツールキットが付属していません。
疑問が浮かびました:Djangoはオールオアナッシングの取引ですか? プロジェクトから完全に削除する必要がありますか、それとも他のマイクロフレームワークや従来のフレームワークの柔軟性と組み合わせる方法を学ぶことができますか? 使用したい部分を選んで選択し、他の部分を避けることはできますか?
両方の長所を活かすことができますか? 特にセッション管理に関しては、そうです。
(言うまでもなく、Djangoフリーランサー向けのプロジェクトはたくさんあります。)
今Pythonチュートリアル:Djangoセッションの共有
この投稿の目的は、ユーザー認証と登録のタスクをDjangoに委任し、Redisを使用して他のフレームワークとユーザーセッションを共有することです。 このようなものが役立ついくつかのシナリオを考えることができます:
- Djangoアプリとは別にRESTAPIを開発する必要がありますが、セッションデータを共有したいと考えています。
- 後で交換するか、何らかの理由でスケールアウトする必要があり、それでもセッションデータが必要な特定のコンポーネントがあります。
このチュートリアルでは、Redisを使用して、2つのフレームワーク(この場合はDjangoとFlask)間でセッションを共有します。 現在の設定では、SQLiteを使用してユーザー情報を保存しますが、必要に応じて、バックエンドをNoSQLデータベース(またはSQLベースの代替データベース)に関連付けることができます。
セッションを理解する
DjangoとFlaskの間でセッションを共有するには、Djangoがセッション情報を保存する方法について少し知る必要があります。 Djangoのドキュメントはかなり良いですが、完全を期すための背景を説明します。
セッション管理の種類
通常、Pythonアプリのセッションデータは次の2つの方法のいずれかで管理することを選択できます。
Cookieベースのセッション:このシナリオでは、セッションデータはバックエンドのデータストアに保存されません。 代わりに、シリアル化され、(SECRET_KEYを使用して)署名され、クライアントに送信されます。 クライアントがそのデータを送り返すと、その整合性が改ざんされていないかチェックされ、サーバー上で再び逆シリアル化されます。
ストレージベースのセッション:このシナリオでは、セッションデータ自体はクライアントに送信されません。 代わりに、セッションストアに保存されている現在のユーザーのIDを示すために、ごく一部(キー)が送信されます。
この例では、後者のシナリオに関心があります。セッションデータをバックエンドに保存してから、Flaskでチェックインする必要があります。 前者でも同じことができますが、Djangoのドキュメントに記載されているように、最初の方法のセキュリティについていくつかの懸念があります。
一般的なワークフロー
セッションの処理と管理の一般的なワークフローは、次の図のようになります。
セッション共有についてもう少し詳しく見ていきましょう。
新しいリクエストが届いたら、最初のステップは、Djangoスタックに登録されているミドルウェアを介してリクエストを送信することです。 ここでは、ご想像のとおり、セッションの管理と処理に関連する
SessionMiddleware
クラスに関心があります。class SessionMiddleware(object): def process_request(self, request): engine = import_module(settings.SESSION_ENGINE) session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None) request.session = engine.SessionStore(session_key)
このスニペットでは、Djangoは登録された
SessionEngine
を取得し(すぐに取得します)、request
からSESSION_COOKIE_NAME
を抽出し(デフォルトではsessionid
)、セッションストレージを処理するために選択したSessionEngine
の新しいインスタンスを作成します。
後で(ユーザービューが処理された後、ミドルウェアスタックに残っている)、セッションエンジンはそのsaveメソッドを呼び出して、データストアへの変更を保存します。 (ビューの処理中に、ユーザーがセッション内でいくつかの変更を行った可能性があります。たとえば、
request.session
を使用してセッションオブジェクトに新しい値を追加します。)次に、SESSION_COOKIE_NAME
がクライアントに送信されます。 簡略化されたバージョンは次のとおりです。def process_response(self, request, response): .... if response.status_code != 500: request.session.save() response.set_cookie(settings.SESSION_COOKIE_NAME, request.session.session_key, max_age=max_age, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, path=settings.SESSION_COOKIE_PATH, secure=settings.SESSION_COOKIE_SECURE or None, httponly=settings.SESSION_COOKIE_HTTPONLY or None) return response
特にSessionEngine
クラスに関心があります。このクラスは、Redisバックエンドとの間でデータを保存およびロードするためのものに置き換えられます。
幸いなことに、これをすでに処理しているプロジェクトがいくつかあります。 これがredis_sessions_forkの例です。 セッションをRedisとの間で(それぞれ)保存およびロードするように記述されたsave
メソッドとload
メソッドに細心の注意を払ってください。
class SessionStore(SessionBase): """ Redis session back-end for Django """ def __init__(self, session_key=None): super(SessionStore, self).__init__(session_key) def _get_or_create_session_key(self): if self._session_key is None: self._session_key = self._get_new_session_key() return self._session_key def load(self): session_data = backend.get(self.session_key) if not session_data is None: return self.decode(session_data) else: self.create() return {} def exists(self, session_key): return backend.exists(session_key) def create(self): while True: self._session_key = self._get_new_session_key() try: self.save(must_create=True) except CreateError: continue self.modified = True self._session_cache = {} return def save(self, must_create=False): session_key = self._get_or_create_session_key() expire_in = self.get_expiry_age() session_data = self.encode(self._get_session(no_load=must_create)) backend.save(session_key, expire_in, session_data, must_create) def delete(self, session_key=None): if session_key is None: if self.session_key is None: return session_key = self.session_key backend.delete(session_key)
セッションデータをロードするにはFlaskに同様の何かを実装する必要があるため、このクラスがどのように動作しているかを理解することが重要です。 REPLの例を詳しく見てみましょう。
>>> from django.conf import settings >>> from django.utils.importlib import import_module >>> engine = import_module(settings.SESSION_ENGINE) >>> engine.SessionStore() <redis_sessions_fork.session.SessionStore object at 0x3761cd0> >>> store["count"] = 1 >>> store.save() >>> store.load() {u'count': 1}
セッションストアのインターフェイスは非常に理解しやすいですが、内部では多くのことが行われています。 Flaskに同様の何かを実装できるように、もう少し深く掘り下げる必要があります。
注:「SessionEngineをFlaskにコピーしないのはなぜですか?」と質問するかもしれません。 言うのは簡単です。 最初に説明したように、DjangoはそのSettingsオブジェクトと緊密に結合されているため、Djangoモジュールをインポートして、追加の作業なしで使用することはできません。
Djangoセッション(De-)Serialization
私が言ったように、Djangoはセッションストレージの複雑さを隠すために多くの作業を行います。 上記のスニペットに保存されているRedisキーを確認してみましょう。

>>> store.session_key u"ery3j462ezmmgebbpwjajlxjxmvt5adu"
それでは、redis-cliでそのキーをクエリしてみましょう。
redis 127.0.0.1:6379> get "django_sessions:ery3j462ezmmgebbpwjajlxjxmvt5adu" "ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ=="
ここに表示されているのは、Base64でエンコードされた非常に長い文字列です。 その目的を理解するには、DjangoのSessionBase
クラスを調べて、その処理方法を確認する必要があります。
class SessionBase(object): """ Base class for all Session classes. """ def encode(self, session_dict): "Returns the given session dictionary serialized and encoded as a string." serialized = self.serializer().dumps(session_dict) hash = self._hash(serialized) return base64.b64encode(hash.encode() + b":" + serialized).decode('ascii') def decode(self, session_data): encoded_data = base64.b64decode(force_bytes(session_data)) try: hash, serialized = encoded_data.split(b':', 1) expected_hash = self._hash(serialized) if not constant_time_compare(hash.decode(), expected_hash): raise SuspiciousSession("Session data corrupted") else: return self.serializer().loads(serialized) except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions if isinstance(e, SuspiciousOperation): logger = logging.getLogger('django.security.%s' % e.__class__.__name__) logger.warning(force_text(e)) return {}
encodeメソッドは、最初に現在登録されているシリアライザーを使用してデータをシリアル化します。 つまり、セッションを文字列に変換し、後でセッションに戻すことができます(詳細については、SESSION_SERIALIZERのドキュメントを参照してください)。 次に、シリアル化されたデータをハッシュし、後でこのハッシュを署名として使用して、セッションデータの整合性をチェックします。 最後に、そのデータペアをBase64でエンコードされた文字列としてユーザーに返します。
ちなみに、バージョン1.6より前では、Djangoはデフォルトでセッションデータのシリアル化にpickleを使用していました。 セキュリティ上の懸念から、デフォルトのシリアル化方法はdjango.contrib.sessions.serializers.JSONSerializer
になりました。
サンプルセッションのエンコード
セッション管理プロセスの動作を見てみましょう。 ここでは、セッションディクショナリは単純にカウントと整数になりますが、これがより複雑なユーザーセッションにどのように一般化されるかを想像できます。
>>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{"count":1}'
ストアメソッド(u'ZmUxOTY…==')の結果は、シリアル化されたユーザーセッションとそのハッシュを含むエンコードされた文字列です。 デコードすると、実際にハッシュ('fe1964e…')とセッション( {"count":1}
)の両方が返されます。
デコードメソッドは、ハッシュがそのセッションに対して正しいことを確認し、Flaskで使用するときにデータの整合性を保証することに注意してください。 私たちの場合、クライアント側でセッションが改ざんされることについてあまり心配していません。理由は次のとおりです。
Cookieベースのセッションを使用していません。つまり、すべてのユーザーデータをクライアントに送信しているわけではありません。
Flaskでは、指定されたキーが存在するかどうかを通知し、保存されたデータを返す読み取り専用の
SessionStore
が必要になります。
フラスコへの拡張
次に、Flaskで動作するRedisセッションエンジン(データベース)の簡略化されたバージョンを作成しましょう。 基本クラスと同じSessionStore
(上記で定義)を使用しますが、不正な署名のチェックやセッションの変更など、その機能の一部を削除する必要があります。 Djangoから保存されたセッションデータをロードする読み取り専用のSessionStore
にもっと興味があります。 それがどのように組み合わされるかを見てみましょう:
class SessionStore(object): # The default serializer, for now def __init__(self, conn, session_key, secret, serializer=None): self._conn = conn self.session_key = session_key self._secret = secret self.serializer = serializer or JSONSerializer def load(self): session_data = self._conn.get(self.session_key) if not session_data is None: return self._decode(session_data) else: return {} def exists(self, session_key): return self._conn.exists(session_key) def _decode(self, session_data): """ Decodes the Django session :param session_data: :return: decoded data """ encoded_data = base64.b64decode(force_bytes(session_data)) try: # Could produce ValueError if there is no ':' hash, serialized = encoded_data.split(b':', 1) # In the Django version of that they check for corrupted data # I don't find it useful, so I'm removing it return self.serializer().loads(serialized) except Exception as e: # ValueError, SuspiciousOperation, unpickling exceptions. If any of # these happen, return an empty dictionary (ie, empty session). return {}
ロードメソッドはストレージの読み取り専用実装であるため、必要なのはload
メソッドだけです。 つまり、Flaskから直接ログアウトすることはできません。 代わりに、このタスクをDjangoにリダイレクトすることをお勧めします。 ここでの目標は、これら2つのPythonフレームワーク間のセッションを管理して、柔軟性を高めることです。
フラスコセッション
FlaskマイクロフレームワークはCookieベースのセッションをサポートします。つまり、すべてのセッションデータがクライアントに送信され、Base64でエンコードされ、暗号で署名されます。 しかし実際には、Flaskのセッションサポートにはあまり関心がありません。
必要なのは、Djangoによって作成されたセッションIDを取得し、それをRedisバックエンドと照合して、リクエストが事前に署名されたユーザーに属していることを確認することです。 要約すると、理想的なプロセスは次のようになります(これは上の図と同期します)。
- ユーザーのCookieからDjangoセッションIDを取得します。
- セッションIDがRedisで見つかった場合、そのIDに一致するセッションを返します。
- そうでない場合は、ログインページにリダイレクトします。
その情報をチェックし、現在のuser_id
をFlaskのg
変数に設定するデコレータがあると便利です。
from functools import wraps from flask import g, request, redirect, url_for def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): djsession_id = request.cookies.get("sessionid") if djsession_id is None: return redirect("/") key = get_session_prefixed(djsession_id) session_store = SessionStore(redis_conn, key) auth = session_store.load() if not auth: return redirect("/") g.user_id = str(auth.get("_auth_user_id")) return f(*args, **kwargs) return decorated_function
上記の例では、RedisからDjangoデータをフェッチするために、以前に定義したSessionStore
を引き続き使用しています。 セッションに_auth_user_id
がある場合、ビュー関数からコンテンツを返します。 それ以外の場合、ユーザーは必要に応じてログインページにリダイレクトされます。
物事を接着する
Cookieを共有するには、WSGIサーバーを介してDjangoとFlaskを起動し、それらを接着するのが便利だと思います。 この例では、CherryPyを使用しました。
from app import app from django.core.wsgi import get_wsgi_application application = get_wsgi_application() d = wsgiserver.WSGIPathInfoDispatcher({ "/":application, "/backend":app }) server = wsgiserver.CherryPyWSGIServer(("127.0.0.1", 8080), d)
これにより、Djangoは「/」でサービスを提供し、Flaskは「/backend」エンドポイントでサービスを提供します。
結論は
DjangoとFlaskを比較したり、Flaskのマイクロフレームワークを学ぶことだけを勧めたりするのではなく、DjangoとFlaskを統合し、タスクをDjangoに委任することで、認証のために同じセッションデータを共有できるようにしました。 Djangoには、ユーザー登録、ログイン、ログアウトを解決するためのモジュールが多数付属しているため(ほんの数例)、これら2つのフレームワークを組み合わせると、Flaskのような管理しやすいマイクロフレームワークをハックする機会を提供しながら、貴重な時間を節約できます。