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



我的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 请求时,我会收到CSRF verification failed消息Forbidden 403 errors

如果我继续刷新页面,当我幸运的时候,我的 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 文档中的 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>

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

SECRET_KEY = generate_a_random_string()

非常感谢 Alasdair,我再次仔细查看了关于 Django 如何进行 CRSF 验证的官方文档,并特别注意到了这些位:

一个隐藏的表单字段,名称为"csrfmiddlewaretoken"存在于所有 传出的开机自检表单。此字段的值再次为秘密,其中既添加盐又用于炒菜 它。盐在每次调用 get_token(( 时都会再生,以便 表单字段值在每个此类响应中都会更改。

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

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

此检查由 CsrfView Middleware 完成

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

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

最新更新