使用Django Rest Framework时如何访问请求正文并避免获取RawPostDataException

Krz*_*iek 2 python django rest http-post django-rest-framework

我需要获取POST请求正文的原始内容,但是当我尝试访问时却request.body遇到异常:

django.http.request.RawPostDataException:
You cannot access body after reading from request's data stream
Run Code Online (Sandbox Code Playgroud)

我知道建议request.data不要request.body使用Django Rest Framework,而要使用它,但是为了验证数字签名,我必须将请求正文以原始且未经修饰的形式使用,因为这是3rd-party签名并我需要验证的内容。

伪代码:

3rd_party_sign(json_data + secret_key) != validate_sign(json.dumps(request.data) + secret_key)

3rd_party_sign(json_data + secret_key) == validate_sign(request.body + secret_key)
Run Code Online (Sandbox Code Playgroud)

Krz*_*iek 17

我在 DRFs GitHub 上发现了一个有趣的话题,但它并没有完全涵盖这个问题。我调查了这个案子并提出了一个巧妙的解决方案。令人惊讶的是,SO 上没有这样的问题,所以我决定按照SO self-answer Guidelines将其添加给公众。

理解问题和解决方案的关键是HttpRequest.body( source )是如何工作的:

@property
def body(self):
    if not hasattr(self, '_body'):
        if self._read_started:
            raise RawPostDataException("You cannot access body after reading from request's data stream")
        # (...)
        try:
            self._body = self.read()
        except IOError as e:
            raise UnreadablePostError(*e.args) from e
        self._stream = BytesIO(self._body)
    return self._body
Run Code Online (Sandbox Code Playgroud)

访问时body- 如果self._body已经设置,则简单返回,否则正在读取内部请求流并将其分配给 _body: self._body = self.read()。此后任何进一步的访问body都回落到return self._body. 此外,在读取内部请求流之前,有一个if self._read_started检查,如果“读取已开始”,则会引发异常。

所述self._read_startedflague被设定由read()方法():

def read(self, *args, **kwargs):
    self._read_started = True
    try:
        return self._stream.read(*args, **kwargs)
    except IOError as e:
        six.reraise(UnreadablePostError, ...)
Run Code Online (Sandbox Code Playgroud)

现在应该很清楚,如果只调用了方法而不将其结果分配给 requests RawPostDataExceptionrequest.body则在访问之后将引发。read()self._body

现在让我们看看 DRFJSONParser类(源代码):

class JSONParser(BaseParser):
    media_type = 'application/json'
    renderer_class = renderers.JSONRenderer

    def parse(self, stream, media_type=None, parser_context=None):
        parser_context = parser_context or {}
        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
        try:
            data = stream.read().decode(encoding)
            return json.loads(data)
        except ValueError as exc:
            raise ParseError('JSON parse error - %s' % six.text_type(exc))
Run Code Online (Sandbox Code Playgroud)

(我选择了稍微旧的版本 o DRF 源,因为在 2017 年 5 月之后有一些性能改进掩盖了理解我们问题的关键线)

现在应该清楚stream.read()调用设置了_read_started标志,因此body属性不可能再次访问流(在解析器之后)。

解决方案

“无 request.body”方法是 DRF 的意图(我猜),所以尽管在技术上可以启用request.body全局访问(通过自定义中间件) - 它不应该在没有深入了解其所有后果的情况下完成。

request.body可以通过以下方式在本地明确授予对财产的访问权:

您需要定义自定义解析器

import json
from django.conf import settings
from rest_framework.exceptions import ParseError
from rest_framework import renderers
from rest_framework.parsers import BaseParser

class MyJSONParser(BaseParser):
    media_type = 'application/json'
    renderer_class = renderers.JSONRenderer

    def parse(self, stream, media_type=None, parser_context=None):
        parser_context = parser_context or {}
        encoding = parser_context.get('encoding', settings.DEFAULT_CHARSET)
        request = parser_context.get('request')
        try:
            data = stream.read().decode(encoding)
            setattr(request, 'raw_body', data) # setting a 'body' alike custom attr with raw POST content
            return json.loads(data)
        except ValueError as exc:
            raise ParseError('JSON parse error - %s' % six.text_type(exc))
Run Code Online (Sandbox Code Playgroud)

然后可以在需要访问原始请求内容时使用:

@api_view(['POST'])
@parser_classes((MyJSONParser,))
def example_view(request, format=None):
    return Response({'received data': request.raw_body})
Run Code Online (Sandbox Code Playgroud)

虽然request.body仍然在全球范围内无法访问(正如 DRF 作者的意图)。


Gum*_*bah 5

我可能在这里丢失了一些东西,但是我敢肯定,在这种情况下,您不需要定义自定义解析器...

您可以只使用DRF本身的JSONParser:

    from rest_framework.decorators import api_view
    from rest_framework.decorators import parser_classes
    from rest_framework.parsers import JSONParser

    @api_view(['POST']) 
    @parser_classes((JSONParser,)) 
    def example_view(request, format=None):
        """
        A view that can accept POST requests with JSON content.
        """
        return Response({'received data': request.data})
Run Code Online (Sandbox Code Playgroud)

  • 请注意,在我的例子中,我不想访问“request.data”,而是访问原始的“request.body”。请求“data”返回带有解析后的 json 的 python 字典。对于veryfing 签名,我必须以字符串形式检索原始请求正文。 (3认同)

hnd*_*ndr 5

自从提出这个问题以来已经有一段时间了,所以我不确定当时的框架是否存在一些差异,但是如果有人正在从解析器上的DRF 文档中搜索使用最新版本访问原始请求正文:

视图的有效解析器集始终定义为类列表。访问 request.data 时,REST 框架将检查传入请求的 Content-Type 标头,并确定使用哪个解析器来解析请求内容。

这意味着解析器在request.data访问时会延迟执行。因此,解决方案可以非常简单地读取request.body,并在访问之前将其缓存在某处request.data。无需编写自定义解析器。

def some_action(self, request):
  raw_body = request.body
  parsed_body = request.data['something']
  verify_signature(raw_body, request.data['key_or_something'])
Run Code Online (Sandbox Code Playgroud)

  • 你可能以为姜戈会意识到真实身体的重要性。这表明 Django 控制和隐藏一切的态度,它实际上丢失了这个数据。你不需要它吧?我们为您做一切。不,我希望你的解决方案有效。当您开始处理中间件时,您需要很早就保存主体,甚至在有任何数据之前,然后如果以后需要访问主体,则需要与其他组件一起组织。在现有的应用程序中它会变得很尴尬。如果 Django 打算做所有事情,它也应该正式保存原始主体。 (2认同)