【问题标题】:Django CSRF Verification Failed when using WSGIDaemonProcess with multiple processes将 WSGIDaemonProcess 与多个进程一起使用时,Django CSRF 验证失败
【发布时间】:2019-08-15 12:46:57
【问题描述】:

我的 Django 版本是 2.2,我已经配置 httpd.conf (Apache/2.4.39 (Unix) mod_auth_gssapi/1.6.1 mod_wsgi/4.6.5 Python/3.7) 来启动一个进程池,如下所示:

WSGIDaemonProcess my_app processes=6 threads=12 display-name=%{GROUP} home=/tmp maximum-requests=1000 inactivity-timeout=1800 request-timeout=500 python-path=${MY_ENV_VAR}

使用此设置,当我发出 POST 请求时,我会收到 Forbidden 403 errorsCSRF verification failed 消息。

如果我不断刷新页面,当我幸运的时候,我的 POST 请求就会成功。现在我猜这是因为请求可能已经通过具有“正确缓存”的 Django 实例到达 wsgi 进程。

为了测试上述理论,如果我将 httpd.conf 更改为只有 1 个池进程,如下所示,我不会收到 403 错误。

WSGIDaemonProcess my_app processes=1 threads=12 display-name=%{GROUP} home=/tmp maximum-requests=1000 inactivity-timeout=1800 request-timeout=500 python-path=${MY_ENV_VAR}

我已按照Django documentation 上的 CSRF 特定说明进行操作,并在 CSRF_USE_SESSIONS 设置为 TrueFalse 时尝试了两种方案

如果我打算为每个将运行我的 Django 应用程序的 wsgi 进程启动一个池,那么配置 Django 的推荐方法是什么?

我做了一些测试,并确认我的 views.py 在 403 错误发生时没有被调用。所以 Django 在尝试调用 views.py 之前就拒绝了 POST 请求。下面是views.py的sn-p:

def my_func(request):
    if request.is_ajax() and request.method == 'POST':
        # some logic

        try:
            response = ... # some logic to build response
            return HttpResponse(json.dumps(response), content_type="application/json")
        except Exception as e:
            return HttpResponseBadRequest('\n'.join(errors))

    return render(request, 'my_app/my_template.html', locals())

使用的模板有一个简单的形式:

<form class="form-horizontal" role="form">{% csrf_token %}

要提交表单,有一个 AJAX 调用:

$.ajax({
    type: "post",
    url: ".",
    data: data,
    cache: false,
    processData: false,
    contentType: false,
    success: function(response) {
        // some code
    },
    error: function(xhr, status, e) {
        // some code
    }
});

这是我的 httpd.conf 的 sn-p

LimitRequestFieldSize 50000
LimitRequestLine      50000
TimeOut 600

LoadModule access_compat_module            modules/mod_access_compat.so
LoadModule alias_module                    modules/mod_alias.so
LoadModule auth_gssapi_module              modules/mod_auth_gssapi.so
LoadModule authn_core_module               modules/mod_authn_core.so
LoadModule authz_core_module               modules/mod_authz_core.so
LoadModule authz_host_module               modules/mod_authz_host.so
LoadModule authz_user_module               modules/mod_authz_user.so
LoadModule filter_module                   modules/mod_filter.so
LoadModule info_module                     modules/mod_info.so
LoadModule log_config_module               modules/mod_log_config.so
LoadModule mime_module                     modules/mod_mime.so
LoadModule mpm_prefork_module              modules/mod_mpm_prefork.so
LoadModule negotiation_module              modules/mod_negotiation.so
LoadModule status_module                   modules/mod_status.so
LoadModule unixd_module                    modules/mod_unixd.so
LoadModule wsgi_module                     modules/mod_wsgi.so
LoadModule setenvif_module                 modules/mod_setenvif.so
LoadModule env_module                      modules/mod_env.so
LoadModule include_module                  modules/mod_include.so

WSGISocketPrefix ${MY_SOCKET_PREFIX}
WSGILazyInitialization On
WSGIRestrictEmbedded On
WSGIScriptReloading On


WSGIDaemonProcess my_app processes=6 threads=12 display-name=%{GROUP} home=/tmp maximum-requests=1000 inactivity-timeout=1800 request-timeout=500 python-path=${HTTPD_my_app_PYTHONPATH}

#If I use the below that has processes=1 instead I do not get CSRF verification failure
#WSGIDaemonProcess my_app processes=1 threads=12 display-name=%{GROUP} home=/tmp maximum-requests=1000 inactivity-timeout=1800 request-timeout=500 python-path=${HTTPD_my_app_PYTHONPATH}

Listen ${HTTPD_my_app_PORT}
<VirtualHost *:${HTTPD_my_app_PORT}>

    Redirect / http://${MY_HOST}:${HTTPD_my_app_PORT_REDIRECT}/
</VirtualHost>

Listen ${HTTPD_my_app_PORT_REDIRECT}
<VirtualHost *:${HTTPD_my_app_PORT_REDIRECT}>
    WSGIScriptAlias / ${HTTPD_my_app_BASE_DIR}/my_app_dir/wsgi.py
    WSGIProcessGroup my_app
    WSGIApplicationGroup %{GLOBAL}

    <Directory "${HTTPD_my_app_BASE_DIR}/my_app_dir/">
      <Files wsgi.py>
        AuthType GSSAPI
        AuthName "MY_ENTITY/GSSAPI"
        GssapiCredStore keytab:${MY_KEYTAB}
        GssapiAllowedMech krb5
        GssapiPublishErrors On

        SetHandler wsgi-script
        Require valid-user
      </Files>
    </Directory>

    <Directory ${HTTPD_my_app_BASE_DIR}/my_app_dir/media>
        Require all granted
    </Directory>

    <Directory ${HTTPD_my_app_BASE_DIR}/static_files>
       Require all granted
    </Directory>
</VirtualHost>

【问题讨论】:

  • “右缓存”是什么意思? Django CSRF 保护不使用缓存。如果您显示您的视图和模板,它可能会显示问题所在。如果您认为可能是 mod_auth_gssapi 导致了问题,那么我会添加有关您如何实现该问题的更多信息。
  • 感谢您的评论。我添加了更多信息。 django 进程必须以某种方式知道令牌才能进行验证 - 所以“正确的缓存”只是指 django 进程知道的关于令牌的任何知识。因为有一个 django 进程池,我只是猜测并非所有 django 进程都知道令牌。最后,我只是好奇为什么当我设置 processes=1 时没有 CSRF 验证问题,而我将进程设置为多个我开始看到问题?
  • Django 不跟踪令牌。它将 cookie 与隐藏表单字段/HTTP 标头中的值进行比较。见how it works
  • 从您提供的链接中了解以上内容,为什么processes参数设置为大于1时不起作用,而processes=1时起作用?
  • 我相信我只是想通了 - 添加了一个答案。请让我知道你的想法。非常感谢您的帮助。

标签: django apache mod-wsgi wsgi


【解决方案1】:

这个问题原来是由于在settings.py 文件中以编程方式生成的随机值被分配给SECRET_KEY - 基本上这是在settings.py 中完成的:

SECRET_KEY = generate_a_random_string()

非常感谢 Alasdair,我又仔细查看了how Django does CRSF verification 上的官方文档,并特别注意到了这些内容:

一个名为“csrfmiddlewaretoken”的隐藏表单字段存在于所有 传出的 POST 表单。该字段的值再次是 秘密,加了盐,用来炒 它。每次调用 get_token() 时都会重新生成盐,以便 每次这样的响应都会更改表单字段值。

对于所有未使用 HTTP GET、HEAD、OPTIONS 的传入请求 或 TRACE,必须存在 CSRF cookie,并且“csrfmiddlewaretoken” 字段必须存在且正确。如果不是,用户将获得一个 403 错误。

在验证“csrfmiddlewaretoken”字段值时,只有 秘密,而不是完整的令牌,与cookie中的秘密进行比较 价值。这允许使用不断变化的令牌。虽然每个请求 可以使用自己的令牌,秘密对所有人都是共同的

这项检查由 CsrfViewMiddleware 完成

所以我的问题是 Apache 正在启动多个 wsgi Django 实例/进程,但它们每个都有不同的 SECRET_KEY。因此,尽管 Django 本身并不真正跟踪完整的令牌,但 Django 实例确实需要知道/拥有相应的密钥,以及一些不可食用的盐,组成令牌。

因此问题的解决方案是使 SECRET_KEY 在 Apache 启动的所有 Django 进程中都相同。

【讨论】:

    猜你喜欢
    • 2014-10-20
    • 2012-09-08
    • 2015-06-02
    • 1970-01-01
    • 2013-09-23
    • 2017-03-29
    相关资源
    最近更新 更多