将WSGIDaemonProcess与多个进程一起使用时,Django CSRF验证失败

时间:2019-08-15 12:46:57

标签: django apache mod-wsgi wsgi

我的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时都尝试了这两种情况

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

我进行了一些测试,并确认发生403错误时未调用我的views.py。因此,Django在尝试调用views.py之前就拒绝了POST请求。以下是views.py的代码段:

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的摘录

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>

1 个答案:

答案 0 :(得分:1)

事实证明,该问题是由于在SECRET_KEY文件中将以编程方式生成的随机值分配给了settings.py的缘故-本质上是在settings.py中完成的:

SECRET_KEY = generate_a_random_string()

非常感谢Alasdair,我再次仔细查看了how Django does CRSF verification上的官方文档,并特别注意了以下几点:

  

一个隐藏的表单字段,名称为“ csrfmiddlewaretoken”   外发POST表单。同样,此字段的值是   秘诀,并在其中添加了盐并用于加扰   它。每次调用get_token()都会重新生成盐,以便   每个此类响应中都会更改表单字段值。

     

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

     

验证“ csrfmiddlewaretoken”字段值时,仅   (不是完整令牌)与Cookie中的秘密进行比较   值。这允许使用不断变化的令牌。虽然每个请求   可能会使用自己的令牌,这个秘密仍然是所有人的共同点

     

此检查由CsrfViewMiddleware 完成。

所以我的问题是Apache正在分解多个wsgi Django实例/进程,但是每个实例都有一个不同的SECRET_KEY。因此,尽管Django并没有真正跟踪完整的令牌,但是Django实例确实需要知道/拥有对应的秘密密钥,该密钥与一些不可食用的盐一起构成了令牌。

因此,解决该问题的方法是使SECRET_KEY与Apache启动的所有Django进程相同。