Django - Последовательный запрос переопределяет атрибут сессии
Я хочу генерировать счет-фактуру для каждого заказа, и в некоторых случаях есть два сгенерированных счета-фактуры для одного заказа.
В этих случаях первый запрос счета не проходит и я получаю ошибку 400 с сообщением "недействительная подпись" (как я определил в логике представления), в то время как второй остается успешным.
Мой views.py
from django.core import signing
from django.contrib import messages
class OrderView(MyMultiFormsView):
forms = {‘create’: OrderForm}
… # view logic for get method: display a list of orders
def post(self, request, *args, **kwargs):
form = self.forms[‘create’](request.POST, request.FILES, prefix=‘create’)
if form.is_valid():
order = form.save()
messages.success(request, signing.dump(order.id))
if order.is_paid:
messages.info(request, signing.dump(order.id))
return redirect(request.get_full_path())
… # not valid: render form and show errors
class ExportView(View):
http_method_names = [‘get’]
actions = {
'invoice': {
'url_token': 'print-invoice',
'session_prop': '_invoice_token',
},
'receipt': {
'url_token': 'print-receipt',
'session_prop': '_receipt_token',
},
}
def get(self, request, *args, **kwargs):
if kwargs['action'] not in self.actions:
return render(request, '400.html', {'msg': 'undefined action'}, status=400)
action = self.actions[kwargs['action']]
if kwargs['token'] == action['url_token']:
try:
token = request.session.pop(action['session_prop'])
sign = signing.load(token)
except:
return render(request, '400.html', {'msg': 'invalid signature', status=400)
return getattr(self, kwargs['action'])(sign)
else:
request.session[action['session_prop']] = kwargs['token']
print(request.session.__dict__)
redirect_url = request.path.replace(kwargs['token'], action['url_token'])
return redirect(redirect_url)
def invoice(self, sign):
inv = Invoice(sign)
# inv.as_file() returns a pdf file with io.BytesIO object type
return FileResponse(inv.as_file(), filename=inv.filename)
def receipt(self, sign):
rcp = Receipt(sign)
return FileResponse(rcp.as_file(), filename=rcp.filename)
Мой шаблон orders.html
<!-- template logic to render orders -->
<script>
...
{% if messages %}
{% for msg in messages %}
{% if msg.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
window.open("{% url 'sale:export' 'invoice' msg %}", "_blank");
{% elif msg.level == DEFAULT_MESSAGE_LEVELS.INFO %}
window.open("{% url 'sale:export' 'receipt' msg %}", "_blank");
{% endif %]
{% endfor %}
{% endif %}
</script>
Мой urls.py
app_name = 'sale'
urls = [
...
path('orders/', views.OrderView.as_view(), name='orders'),
path('orders/export/<action>/<token>/', views.ExportView.as_view(), name='export'),
]
Я использую сервер разработки Django, и print(request.session.__dict__)
показывает
{'_SessionBase__session_key': 'ljm62w2z50jrdlolemrthk6bu9btjxry', 'accessed': True, 'modified': True, 'serializer': <class 'django.core.signing.JSONSerializer'>, 'model': <class 'django.contrib.sessions.models.Session'>, '_session_cache': {'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': 'c0d671f349e6efdadfe3afae7e1e21eb5e82c16e2a363d6706984a6a1d755c55', '_invoice_token': 'NTgxMzE:1oA3vp:iyEhWL3HV6Ai1xOgogB5yDQ6fMtEUE5eKgr4aGLQonU'}}
{'_SessionBase__session_key': 'ljm62w2z50jrdlolemrthk6bu9btjxry', 'accessed': True, 'modified': True, 'serializer': <class 'django.core.signing.JSONSerializer'>, 'model': <class 'django.contrib.sessions.models.Session'>, '_session_cache': {'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': 'c0d671f349e6efdadfe3afae7e1e21eb5e82c16e2a363d6706984a6a1d755c55', '_receipt_token': 'NTgxMzE:1oA3vp:iyEhWL3HV6Ai1xOgogB5yDQ6fMtEUE5eKgr4aGLQonU'}}
[09/Jul/2022 14:27:09] "GET /sale/orders/export/invoice/NTgxMzE:1oA3vp:iyEhWL3HV6Ai1xOgogB5yDQ6fMtEUE5eKgr4aGLQonU/ HTTP/1.1" 302 0
[09/Jul/2022 14:27:09] "GET /sale/orders/export/receipt/NTgxMzE:1oA3vp:iyEhWL3HV6Ai1xOgogB5yDQ6fMtEUE5eKgr4aGLQonU/ HTTP/1.1" 302 0
Bad Request: /sale/orders/export/invoice/print-invoice/
[09/Jul/2022 14:27:09] "GET /sale/orders/export/invoice/print-invoice/ HTTP/1.1" 400 1307
[09/Jul/2022 14:27:09] "GET /sale/orders/export/receipt/print-receipt/ HTTP/1.1" 200 18804
Похоже, что атрибут сессии '_invoice_token' каким-то образом переопределяется.
Почему атрибут сессии переопределяется? И как я могу обойти это?
PS: Удаление всей части редиректа и использование токена подписи непосредственно в url дает желаемые результаты, но это будет последний вариант, так как я хочу сохранить токен как можно более секретным.
Вопрос здесь:
messages.success(request, signing.dump(order.id))
if order.is_paid:
messages.info(request, signing.dump(order.id))
Итак, когда is_paid равно true, генерируется два сообщения, одно для success, другое для info, и оба выполняются. Попробуйте:
if order.is_paid:
messages.info(request, signing.dump(order.id))
else:
messages.success(request, signing.dump(order.id))
Django хранит сессию как JSON и не обрабатывает условия гонки от одновременных запросов.
#10760 (Некоторые данные сессии теряются между несколькими одновременными запросами) был закрыт (недействителен).
Вы можете отменить:
__setitem__
для использования атомарной транзакции, и_get_session_from_db
для выполненияSELECT ... FOR UPDATE
.
# mysite/session.py
import logging
from django.contrib.sessions.backends import db
from django.core.exceptions import SuspiciousOperation
from django.db import transaction
from django.utils import timezone
class SessionStore(db.SessionStore):
def __setitem__(self, key, value):
# self._session[key] = value # -
# self.modified = True # -
with transaction.atomic(): # +
self._session[key] = value # +
self.save() # +
def _get_session_from_db(self):
queryset = self.model.objects # +
if transaction.get_connection().in_atomic_block: # +
queryset = queryset.select_for_update() # +
try:
# return self.model.objects.get( # -
return queryset.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
Используйте свой механизм сеанса:
# mysite/settings.py
...
SESSION_ENGINE = 'mysite.session'