Accessing Django Session from Multiple DBs

huangapple go评论71阅读模式
英文:

Accessing Django Session from Multiple DBs

问题

I have a multi-tenant system where I enable Quickbooks Authentication to allow clients to pass accounting data to QBO. Each client has their own DB and session store.

我有一个多租户系统,在其中启用了Quickbooks身份验证,允许客户将会计数据传递给QBO。每个客户都有自己的数据库和会话存储。

I store the authentication data after success in django.sessions, this works well for most instances but using QB OAuth I need to provide a redirect URI and I can only have 25 of them. This isn't an issue now, but it could be once my clients using QBO grows so I would like to provide a redirect URI that is not based on a client URL, however the data I need to access for auth is inside the client session.

我在成功后将身份验证数据存储在django.sessions中,这对大多数情况都有效,但是使用QB OAuth时,我需要提供一个重定向URI,我只能有25个。现在这不是问题,但一旦我的使用QBO的客户增多,可能会成为问题,所以我想提供一个不基于客户URL的重定向URI,但我需要访问客户会话中的数据来进行身份验证。

How do I access the session of a database I define?

我如何访问我定义的数据库的会话?

For example, I often use multi databases in instances like this:

例如,我经常在类似这样的情况下使用多个数据库:

model.objects.using(client_url).all()

How can I do this with Django Sessions using request.session?

如何在使用request.session的Django会话中实现这一点?

英文:

I have a multi-tenant system where I enable Quickbooks Authentication to allow clients to pass accounting data to QBO. Each client has their own DB and session store.

I store the authentication data after success in django.sessions, this works well for most instances but using QB OAuth I need to provide a redirect URI and I can only have 25 of them. This isn't an issue now, but it could be once my clients using QBO grows so I would like to provide a redirect URI that is not based on a client URL, however the data I need to access for auth is inside the client session.

How do I access the session of a database I define?

For example, I often use multi databases in instances like this:

model.objects.using(client_url).all()

How can I do this with Django Sessions using request.session?

答案1

得分: 1

以下是您要翻译的内容:

Okay so as noted in the comments, the solution to this was to subclass the default database session store from here: https://github.com/django/django/blob/main/django/contrib/sessions/backends/db.py so I could then add the `using` argument as needed to alter and set the correct session based on the client's database.

First define your new SessionStore (mine is very similar I just added the using variables in `__init__` and in needed methods):

import logging

from django.contrib.sessions.backends.base import CreateError, SessionBase, UpdateError
from django.core.exceptions import SuspiciousOperation
from django.db import DatabaseError, IntegrityError, router, transaction
from django.utils import timezone
from django.utils.functional import cached_property

class SessionStore(SessionBase):
"""
Implement database session store.
"""

def __init__(self, session_key=None, using=None):
    self.using = using # new, used for finding the right db to use
    super().__init__(session_key)


def set_using(self, using):
    # not actually used because init covers it, but here if needed.
    self.using = using

@classmethod
def get_model_class(cls):
    # Avoids a circular import and allows importing SessionStore when
    # django.contrib.sessions is not in INSTALLED_APPS.
    from django.contrib.sessions.models import Session

    return Session

@cached_property
def model(self):
    return self.get_model_class()

def _get_session_from_db(self):
    try:
        return self.model.objects.using(self.using).get(
            session_key=self.session_key, expire_date__gt=timezone.now()
        )
    except (self.model.DoesNotExist, SuspiciousOperation) as e:
        if isinstance(e, SuspiciousOperation):
            logger = logging.getLogger("django.security.%s" % e.__class__.__name__)
            logger.warning(str(e))
        self._session_key = None

def load(self):
    s = self._get_session_from_db()
    return self.decode(s.session_data) if s else {}

def exists(self, session_key):
    return self.model.objects.using(self.using).filter(session_key=session_key).exists()

def create(self):
    while True:
        self._session_key = self._get_new_session_key()
        try:
            # Save immediately to ensure we have a unique entry in the
            # database.
            self.save(must_create=True)
        except CreateError:
            # Key wasn't unique. Try again.
            continue
        self.modified = True
        return

def create_model_instance(self, data):
    """
    Return a new instance of the session model object, which represents the
    current session state. Intended to be used for saving the session data
    to the database.
    """
    return self.model(
        session_key=self._get_or_create_session_key(),
        session_data=self.encode(data),
        expire_date=self.get_expiry_date(),
    )

def save(self, must_create=False):
    """
    Save the current session data to the database. If 'must_create' is
    True, raise a database error if the saving operation doesn't create a
    new entry (as opposed to possibly updating an existing entry).
    """
    if self.session_key is None:
        return self.create()
    data = self._get_session(no_load=must_create)
    obj = self.create_model_instance(data)
    # use the default using based on router if not directly passed in
    if not self.using:
        self.using = router.db_for_write(self.model, instance=obj)
    # print(using)
    try:
        with transaction.atomic(using=self.using):
            obj.save(
                force_insert=must_create, force_update=not must_create, using=self.using
            )
    except IntegrityError:
        if must_create:
            raise CreateError
        raise
    except DatabaseError:
        if not must_create:
            raise UpdateError
        raise

def delete(self, session_key=None):
    if session_key is None:
        if self.session_key is None:
            return
        session_key = self.session_key
    try:
        self.model.objects.using(self.using).get(session_key=session_key).delete()
    except self.model.DoesNotExist:
        pass

@classmethod
def clear_expired(cls):
    cls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete()

Then somewhere in your settings.py or whatever file, use your custom `SessionStore` as the engine:

`SESSION_ENGINE = "utils.session_store"`

And here is my altered logic...

From the view that I used for authentication, that has no client URL in it, so I don't have direct access to the session database from the router:

```python
def quickbooks_authentication(request):
    # ensure session key is there...we need it here.
    session_key = request.COOKIES.get('session_key')
    client_url = request.COOKIES.get('client_url')
    if not session_key:
        messages.error(request, 'Error, the session_key is not saved. Try again.')
        return redirect(request.META.get('HTTP_REFERER', f"{reverse('index', args=[])}"))
    # now ensure the session store exists, if not we have errors.
    store = SessionStore(session_key=session_key, using=client_url)
    if not store.exists(session_key=session_key):
        messages.error(request, 'Error, the SessionStore did not exist. Try again.')

    # now set the store to request.session,so it persists.
    request.session = store
    ...

...and from there, you can access the session as normal and whatever you edit from store persists when the session is restored on the next view with a definition like:

def closed_quickbooks_batch_detailed(request, client_url):
    print(request.session.values())  # good to go...

<details>
<summary>英文:</summary>

Okay so as noted in the comments, the solution to this was to subclass the default database session store from here: https://github.com/django/django/blob/main/django/contrib/sessions/backends/db.py so I could then add the `using` argument as needed to alter and set the correct session based on the client&#39;s database.

First define your new SessionStore (mine is very similar I just added the using variables in `__init__` and in needed methods):

import logging

from django.contrib.sessions.backends.base import CreateError, SessionBase, UpdateError
from django.core.exceptions import SuspiciousOperation
from django.db import DatabaseError, IntegrityError, router, transaction
from django.utils import timezone
from django.utils.functional import cached_property

class SessionStore(SessionBase):
"""
Implement database session store.
"""

def __init__(self, session_key=None, using=None):
    self.using = using # new, used for finding the right db to use
    super().__init__(session_key)


def set_using(self, using):
    # not actually used because init covers it, but here if needed.
    self.using = using

@classmethod
def get_model_class(cls):
    # Avoids a circular import and allows importing SessionStore when
    # django.contrib.sessions is not in INSTALLED_APPS.
    from django.contrib.sessions.models import Session

    return Session

@cached_property
def model(self):
    return self.get_model_class()

def _get_session_from_db(self):
    try:
        return self.model.objects.using(self.using).get(
            session_key=self.session_key, expire_date__gt=timezone.now()
        )
    except (self.model.DoesNotExist, SuspiciousOperation) as e:
        if isinstance(e, SuspiciousOperation):
            logger = logging.getLogger(&quot;django.security.%s&quot; % e.__class__.__name__)
            logger.warning(str(e))
        self._session_key = None

def load(self):
    s = self._get_session_from_db()
    return self.decode(s.session_data) if s else {}

def exists(self, session_key):
    return self.model.objects.using(self.using).filter(session_key=session_key).exists()

def create(self):
    while True:
        self._session_key = self._get_new_session_key()
        try:
            # Save immediately to ensure we have a unique entry in the
            # database.
            self.save(must_create=True)
        except CreateError:
            # Key wasn&#39;t unique. Try again.
            continue
        self.modified = True
        return

def create_model_instance(self, data):
    &quot;&quot;&quot;
    Return a new instance of the session model object, which represents the
    current session state. Intended to be used for saving the session data
    to the database.
    &quot;&quot;&quot;
    return self.model(
        session_key=self._get_or_create_session_key(),
        session_data=self.encode(data),
        expire_date=self.get_expiry_date(),
    )

def save(self, must_create=False):
    &quot;&quot;&quot;
    Save the current session data to the database. If &#39;must_create&#39; is
    True, raise a database error if the saving operation doesn&#39;t create a
    new entry (as opposed to possibly updating an existing entry).
    &quot;&quot;&quot;
    if self.session_key is None:
        return self.create()
    data = self._get_session(no_load=must_create)
    obj = self.create_model_instance(data)
    # use the default using based on router if not directly passed in
    if not self.using:
        self.using = router.db_for_write(self.model, instance=obj)
    # print(using)
    try:
        with transaction.atomic(using=self.using):
            obj.save(
                force_insert=must_create, force_update=not must_create, using=self.using
            )
    except IntegrityError:
        if must_create:
            raise CreateError
        raise
    except DatabaseError:
        if not must_create:
            raise UpdateError
        raise

def delete(self, session_key=None):
    if session_key is None:
        if self.session_key is None:
            return
        session_key = self.session_key
    try:
        self.model.objects.using(self.using).get(session_key=session_key).delete()
    except self.model.DoesNotExist:
        pass

@classmethod
def clear_expired(cls):
    cls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete()

Then somewhere in your settings.py or whatever file, use your custom `SessionStore` as the engine:

`SESSION_ENGINE = &quot;utils.session_store&quot;`

And here is my altered logic...

From the view that I used for authentication, that has no client URL in it, so I don&#39;t have direct access to the session database from the router:

def quickbooks_authentication(request):
# ensure session key is there...we need it here.
session_key = request.COOKIES.get('session_key')
client_url = request.COOKIES.get('client_url')
if not session_key:
messages.error(request, 'Error, the session_key is not saved. Try again.')
return redirect(request.META.get('HTTP_REFERER', f"{reverse('index', args=[])}"))
# now ensure the session store exists, if not we have errors.
store = SessionStore(session_key=session_key, using=client_url)
if not store.exists(session_key=session_key):
messages.error(request, 'Error, the SessionStore did not exist. Try again.')

# now set the store to request.session,so it persists.
request.session = store
...

...and from there, you can access the session as normal and whatever you edit from `store` persists when the session is restored on the next view with a definition like:

def closed_quickbooks_batch_detailed(request, client_url):
print(request.session.values()) # good to go...






</details>



huangapple
  • 本文由 发表于 2023年7月6日 21:05:38
  • 转载请务必保留本文链接:https://go.coder-hub.com/76629169.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定