Django, Flask, Redis 튜토리얼: Python 프레임워크 간의 웹 애플리케이션 세션 관리

게시 됨: 2022-03-11

Django 대 Flask: Django가 잘못된 선택일 때

저는 많은 개인 및 클라이언트 프로젝트에서 Django를 사랑하고 사용합니다. 주로 더 고전적인 웹 응용 프로그램과 관계형 데이터베이스와 관련된 프로젝트를 위한 것입니다. 그러나 Django는 은총알이 아닙니다.

설계상 Django는 ORM, 템플릿 엔진 시스템 및 설정 개체와 매우 밀접하게 결합되어 있습니다. 또한 새로운 프로젝트가 아닙니다. 이전 버전과의 호환성을 유지하기 위해 많은 짐을 짊어지고 있습니다.

일부 Python 개발자는 이것을 주요 문제로 봅니다. 그들은 Django가 충분히 유연하지 않고 가능하면 피하고 대신 Flask와 같은 Python 마이크로프레임워크를 사용한다고 말합니다.

나는 그 의견을 공유하지 않습니다. Django는 모든 프로젝트 사양에 맞지 않더라도 적절한 장소와 시간에 사용할 때 훌륭합니다. 만트라가 진행하는 대로: "작업에 적합한 도구를 사용하십시오."

(적절한 장소와 시간이 아닐지라도 때때로 Django를 이용한 프로그래밍은 독특한 이점을 가질 수 있습니다.)

어떤 경우에는 더 가벼운 프레임워크(예: Flask)를 사용하는 것이 더 좋을 수 있습니다. 종종 이러한 마이크로프레임워크는 해킹하기가 얼마나 쉬운지 깨닫게 되면 빛을 발하기 시작합니다.

구조를 위한 마이크로프레임웍스

내 클라이언트 프로젝트 중 일부에서 Django를 포기하고 마이크로프레임워크로 이동하는 것에 대해 논의했습니다. 일반적으로 클라이언트가 몇 가지 흥미로운 작업(예: 응용 프로그램 개체에 ZeroMQ 포함) 및 프로젝트를 수행하기를 원할 때 Django를 사용하면 목표를 달성하기가 더 어려워 보입니다.

더 일반적으로 Flask는 다음과 같은 경우에 유용합니다.

  • 간단한 REST API 백엔드
  • 데이터베이스 액세스가 필요하지 않은 애플리케이션
  • NoSQL 기반 웹 앱
  • 사용자 지정 URL 구성과 같은 매우 구체적인 요구 사항이 있는 웹 앱

동시에 우리 앱은 Django가 몇 년 전에 해결한 사용자 등록 및 기타 일반적인 작업을 요구했습니다. 가벼운 무게를 감안할 때 Flask는 동일한 툴킷과 함께 제공되지 않습니다.

질문이 떠올랐습니다. Django가 전부 아니면 전무(all-or-nothing) 거래입니까?

질문이 떠올랐습니다. Django가 전부 아니면 전무(all-or-nothing) 거래입니까? 프로젝트에서 완전히 삭제해야 합니까, 아니면 다른 마이크로프레임워크 또는 기존 프레임워크의 유연성과 결합하는 방법을 배울 수 있습니까? 우리는 우리가 사용하고 싶은 조각을 선택하고 다른 사람을 피할 수 있습니까?

두 세계의 장점을 모두 누릴 수 있습니까? 특히 세션 관리와 관련하여 그렇습니다.

( 말할 것도 없이 Django 프리랜서를 위한 많은 프로젝트가 있습니다.)

이제 Python 자습서: Django 세션 공유

이 게시물의 목표는 사용자 인증 및 등록 작업을 Django에 위임하면서 Redis를 사용하여 다른 프레임워크와 사용자 세션을 공유하는 것입니다. 다음과 같은 것이 유용할 수 있는 몇 가지 시나리오를 생각할 수 있습니다.

  • Django 앱과 별도로 REST API를 개발해야 하지만 세션 데이터를 공유하고 싶습니다.
  • 나중에 교체해야 하거나 어떤 이유로 확장해야 하는 특정 구성 요소가 있고 여전히 세션 데이터가 필요합니다.

이 자습서에서는 Redis를 사용하여 두 프레임워크(이 경우 Django 및 Flask) 간에 세션을 공유합니다. 현재 설정에서는 SQLite를 사용하여 사용자 정보를 저장하지만 필요한 경우 백엔드를 NoSQL 데이터베이스(또는 SQL 기반 대안)에 연결할 수 있습니다.

세션 이해하기

Django와 Flask 간에 세션을 공유하려면 Django가 세션 정보를 저장하는 방법에 대해 약간 알아야 합니다. Django 문서는 꽤 훌륭하지만 완성도를 위해 몇 가지 배경 지식을 제공하겠습니다.

세션 관리 품종

일반적으로 다음 두 가지 방법 중 하나로 Python 앱의 세션 데이터를 관리하도록 선택할 수 있습니다.

  • 쿠키 기반 세션 : 이 시나리오에서 세션 데이터는 백엔드의 데이터 저장소에 저장되지 않습니다. 대신 직렬화되고 SECRET_KEY로 서명되어 클라이언트로 전송됩니다. 클라이언트가 해당 데이터를 다시 보내면 무결성이 변조되었는지 확인하고 서버에서 다시 역직렬화됩니다.

  • 스토리지 기반 세션 : 이 시나리오에서 세션 데이터 자체는 클라이언트로 전송 되지 않습니다 . 대신 세션 저장소에 저장된 현재 사용자의 ID를 나타내기 위해 작은 부분(키)만 전송됩니다.

이 예에서는 후자의 시나리오에 더 관심이 있습니다. 세션 데이터가 백엔드에 저장된 다음 Flask에서 확인되기를 원합니다. 전자에서도 동일한 작업을 수행할 수 있지만 Django 문서에서 언급한 것처럼 첫 번째 방법의 보안에 대한 몇 가지 우려가 있습니다.

일반 워크플로

세션 처리 및 관리의 일반적인 워크플로는 다음 다이어그램과 유사합니다.

Redis를 사용하여 Flask와 Django 간의 사용자 세션 관리를 보여주는 다이어그램.

세션 공유를 좀 더 자세히 살펴보겠습니다.

  1. 새로운 요청이 들어오면 첫 번째 단계는 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 의 새 인스턴스를 생성합니다.

  • 나중에(사용자 보기가 처리된 후, 그러나 여전히 미들웨어 스택에 있음) 세션 엔진은 저장 메소드를 호출하여 데이터 저장소에 대한 변경 사항을 저장합니다. (보기를 처리하는 동안 사용자는 예를 들어 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에서 세션을 (각각) 저장하고 Redis에서 로드하기 위해 작성된 saveload 메소드에 세심한 주의를 기울이십시오.

 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 세션(역)직렬화

내가 말했듯이 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 {}

인코딩 메서드는 먼저 현재 등록된 직렬 변환기로 데이터를 직렬화합니다. 즉, 세션을 문자열로 변환하고 나중에 다시 세션으로 변환할 수 있습니다(자세한 내용은 SESSION_SERIALIZER 문서 참조). 그런 다음 직렬화된 데이터를 해시하고 나중에 이 해시를 서명으로 사용하여 세션 데이터의 무결성을 확인합니다. 마지막으로 해당 데이터 쌍을 Base64로 인코딩된 문자열로 사용자에게 반환합니다.

그건 그렇고: 버전 1.6 이전에는 Django가 기본적으로 세션 데이터 직렬화를 위해 피클을 사용했습니다. 보안 문제로 인해 기본 직렬화 방법은 이제 django.contrib.sessions.serializers.JSONSerializer 입니다.

예제 세션 인코딩

작동 중인 세션 관리 프로세스를 살펴보겠습니다. 여기에서 세션 사전은 단순히 개수와 정수일 것이지만 이것이 더 복잡한 사용자 세션으로 일반화되는 방법을 상상할 수 있습니다.

 >>> store.encode({'count': 1}) u'ZmUxOTY0ZTFkMmNmODA2OWQ5ZjE4MjNhZmQxNDM0MDBiNmQzNzM2Zjp7ImNvdW50IjoxfQ==' >>> base64.b64decode(encoded) 'fe1964e1d2cf8069d9f1823afd143400b6d3736f:{"count":1}'

저장 방법(u'ZmUxOTY…==')의 결과는 직렬화된 사용자 세션 해당 해시를 포함하는 인코딩된 문자열입니다. 디코딩할 때 실제로 해시('fe1964e…')와 세션( {"count":1} )을 모두 반환합니다.

디코드 방법은 해시가 해당 세션에 대해 올바른지 확인하여 Flask에서 사용할 때 데이터 무결성을 보장합니다. 우리의 경우 다음과 같은 이유로 클라이언트 측에서 세션이 변조되는 것에 대해 너무 걱정하지 않습니다.

  • 쿠키 기반 세션을 사용하지 않습니다. 즉, 모든 사용자 데이터를 클라이언트에 보내지 않습니다.

  • 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로 리디렉션할 수 있습니다. 여기서 목표는 두 Python 프레임워크 간의 세션을 관리하여 더 많은 유연성을 제공하는 것입니다.

플라스크 세션

Flask 마이크로프레임워크는 쿠키 기반 세션을 지원합니다. 즉, 모든 세션 데이터가 Base64로 인코딩되고 암호화 서명되어 클라이언트로 전송됩니다. 그러나 실제로 우리는 Flask의 세션 지원에 별로 관심이 없습니다.

우리에게 필요한 것은 Django에서 생성한 세션 ID를 가져와 Redis 백엔드에 대해 확인하여 요청이 미리 서명된 사용자에게 속하는지 확인할 수 있도록 하는 것입니다. 요약하면 이상적인 프로세스는 다음과 같습니다(위의 다이어그램과 동기화됨).

  • 사용자의 쿠키에서 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 가 있으면 보기 기능에서 콘텐츠를 반환합니다. 그렇지 않으면 사용자가 원하는 대로 로그인 페이지로 리디렉션됩니다.

함께 붙이기

쿠키를 공유하려면 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는 사용자 등록, 로그인 및 로그아웃(몇 가지만 들자면)을 해결하기 위한 많은 모듈과 함께 제공되므로 이 두 프레임워크를 결합하면 Flask와 같이 관리 가능한 마이크로프레임워크를 해킹할 기회를 제공하면서 귀중한 시간을 절약할 수 있습니다.