Accessing Django Session from Multiple DBs

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

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:

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

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

答案1

得分: 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.
  2. 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.
"""

  1. def __init__(self, session_key=None, using=None):
  2. self.using = using # new, used for finding the right db to use
  3. super().__init__(session_key)
  4. def set_using(self, using):
  5. # not actually used because init covers it, but here if needed.
  6. self.using = using
  7. @classmethod
  8. def get_model_class(cls):
  9. # Avoids a circular import and allows importing SessionStore when
  10. # django.contrib.sessions is not in INSTALLED_APPS.
  11. from django.contrib.sessions.models import Session
  12. return Session
  13. @cached_property
  14. def model(self):
  15. return self.get_model_class()
  16. def _get_session_from_db(self):
  17. try:
  18. return self.model.objects.using(self.using).get(
  19. session_key=self.session_key, expire_date__gt=timezone.now()
  20. )
  21. except (self.model.DoesNotExist, SuspiciousOperation) as e:
  22. if isinstance(e, SuspiciousOperation):
  23. logger = logging.getLogger("django.security.%s" % e.__class__.__name__)
  24. logger.warning(str(e))
  25. self._session_key = None
  26. def load(self):
  27. s = self._get_session_from_db()
  28. return self.decode(s.session_data) if s else {}
  29. def exists(self, session_key):
  30. return self.model.objects.using(self.using).filter(session_key=session_key).exists()
  31. def create(self):
  32. while True:
  33. self._session_key = self._get_new_session_key()
  34. try:
  35. # Save immediately to ensure we have a unique entry in the
  36. # database.
  37. self.save(must_create=True)
  38. except CreateError:
  39. # Key wasn't unique. Try again.
  40. continue
  41. self.modified = True
  42. return
  43. def create_model_instance(self, data):
  44. """
  45. Return a new instance of the session model object, which represents the
  46. current session state. Intended to be used for saving the session data
  47. to the database.
  48. """
  49. return self.model(
  50. session_key=self._get_or_create_session_key(),
  51. session_data=self.encode(data),
  52. expire_date=self.get_expiry_date(),
  53. )
  54. def save(self, must_create=False):
  55. """
  56. Save the current session data to the database. If 'must_create' is
  57. True, raise a database error if the saving operation doesn't create a
  58. new entry (as opposed to possibly updating an existing entry).
  59. """
  60. if self.session_key is None:
  61. return self.create()
  62. data = self._get_session(no_load=must_create)
  63. obj = self.create_model_instance(data)
  64. # use the default using based on router if not directly passed in
  65. if not self.using:
  66. self.using = router.db_for_write(self.model, instance=obj)
  67. # print(using)
  68. try:
  69. with transaction.atomic(using=self.using):
  70. obj.save(
  71. force_insert=must_create, force_update=not must_create, using=self.using
  72. )
  73. except IntegrityError:
  74. if must_create:
  75. raise CreateError
  76. raise
  77. except DatabaseError:
  78. if not must_create:
  79. raise UpdateError
  80. raise
  81. def delete(self, session_key=None):
  82. if session_key is None:
  83. if self.session_key is None:
  84. return
  85. session_key = self.session_key
  86. try:
  87. self.model.objects.using(self.using).get(session_key=session_key).delete()
  88. except self.model.DoesNotExist:
  89. pass
  90. @classmethod
  91. def clear_expired(cls):
  92. cls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete()
  1. Then somewhere in your settings.py or whatever file, use your custom `SessionStore` as the engine:
  2. `SESSION_ENGINE = "utils.session_store"`
  3. And here is my altered logic...
  4. 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:
  5. ```python
  6. def quickbooks_authentication(request):
  7. # ensure session key is there...we need it here.
  8. session_key = request.COOKIES.get('session_key')
  9. client_url = request.COOKIES.get('client_url')
  10. if not session_key:
  11. messages.error(request, 'Error, the session_key is not saved. Try again.')
  12. return redirect(request.META.get('HTTP_REFERER', f"{reverse('index', args=[])}"))
  13. # now ensure the session store exists, if not we have errors.
  14. store = SessionStore(session_key=session_key, using=client_url)
  15. if not store.exists(session_key=session_key):
  16. messages.error(request, 'Error, the SessionStore did not exist. Try again.')
  17. # now set the store to request.session,so it persists.
  18. request.session = store
  19. ...

...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:

  1. def closed_quickbooks_batch_detailed(request, client_url):
  2. print(request.session.values()) # good to go...
  1. <details>
  2. <summary>英文:</summary>
  3. 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.
  4. 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.
"""

  1. def __init__(self, session_key=None, using=None):
  2. self.using = using # new, used for finding the right db to use
  3. super().__init__(session_key)
  4. def set_using(self, using):
  5. # not actually used because init covers it, but here if needed.
  6. self.using = using
  7. @classmethod
  8. def get_model_class(cls):
  9. # Avoids a circular import and allows importing SessionStore when
  10. # django.contrib.sessions is not in INSTALLED_APPS.
  11. from django.contrib.sessions.models import Session
  12. return Session
  13. @cached_property
  14. def model(self):
  15. return self.get_model_class()
  16. def _get_session_from_db(self):
  17. try:
  18. return self.model.objects.using(self.using).get(
  19. session_key=self.session_key, expire_date__gt=timezone.now()
  20. )
  21. except (self.model.DoesNotExist, SuspiciousOperation) as e:
  22. if isinstance(e, SuspiciousOperation):
  23. logger = logging.getLogger(&quot;django.security.%s&quot; % e.__class__.__name__)
  24. logger.warning(str(e))
  25. self._session_key = None
  26. def load(self):
  27. s = self._get_session_from_db()
  28. return self.decode(s.session_data) if s else {}
  29. def exists(self, session_key):
  30. return self.model.objects.using(self.using).filter(session_key=session_key).exists()
  31. def create(self):
  32. while True:
  33. self._session_key = self._get_new_session_key()
  34. try:
  35. # Save immediately to ensure we have a unique entry in the
  36. # database.
  37. self.save(must_create=True)
  38. except CreateError:
  39. # Key wasn&#39;t unique. Try again.
  40. continue
  41. self.modified = True
  42. return
  43. def create_model_instance(self, data):
  44. &quot;&quot;&quot;
  45. Return a new instance of the session model object, which represents the
  46. current session state. Intended to be used for saving the session data
  47. to the database.
  48. &quot;&quot;&quot;
  49. return self.model(
  50. session_key=self._get_or_create_session_key(),
  51. session_data=self.encode(data),
  52. expire_date=self.get_expiry_date(),
  53. )
  54. def save(self, must_create=False):
  55. &quot;&quot;&quot;
  56. Save the current session data to the database. If &#39;must_create&#39; is
  57. True, raise a database error if the saving operation doesn&#39;t create a
  58. new entry (as opposed to possibly updating an existing entry).
  59. &quot;&quot;&quot;
  60. if self.session_key is None:
  61. return self.create()
  62. data = self._get_session(no_load=must_create)
  63. obj = self.create_model_instance(data)
  64. # use the default using based on router if not directly passed in
  65. if not self.using:
  66. self.using = router.db_for_write(self.model, instance=obj)
  67. # print(using)
  68. try:
  69. with transaction.atomic(using=self.using):
  70. obj.save(
  71. force_insert=must_create, force_update=not must_create, using=self.using
  72. )
  73. except IntegrityError:
  74. if must_create:
  75. raise CreateError
  76. raise
  77. except DatabaseError:
  78. if not must_create:
  79. raise UpdateError
  80. raise
  81. def delete(self, session_key=None):
  82. if session_key is None:
  83. if self.session_key is None:
  84. return
  85. session_key = self.session_key
  86. try:
  87. self.model.objects.using(self.using).get(session_key=session_key).delete()
  88. except self.model.DoesNotExist:
  89. pass
  90. @classmethod
  91. def clear_expired(cls):
  92. cls.get_model_class().objects.filter(expire_date__lt=timezone.now()).delete()
  1. Then somewhere in your settings.py or whatever file, use your custom `SessionStore` as the engine:
  2. `SESSION_ENGINE = &quot;utils.session_store&quot;`
  3. And here is my altered logic...
  4. 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.')

  1. # now set the store to request.session,so it persists.
  2. request.session = store
  3. ...
  1. ...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...

  1. </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:

确定