Django 4.2语言切换POST请求含CSRF仍返回403 Forbidden问题
Django 4.2 i18n语言切换首次请求403 Forbidden问题
问题现象
在基于Django 4.2的网站中使用i18n库实现语言切换功能,已在根urls.py配置相关路由,并在模板中编写了包含{% csrf_token %}的语言切换表单,但首次执行切换操作时返回403 Forbidden错误,服务器日志提示:
Forbidden (CSRF cookie not set.): /i18n/setlang/
首次触发403后再次切换可成功,私有窗口也存在相同现象:请求头中可见csrftoken,但首次切换时无lang cookie,成功后该cookie出现。
相关配置代码
urls.py配置
urlpatterns = [ path('i18n/', include('django.conf.urls.i18n')), ]+ static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += i18n_patterns( path('',apiView.WebsiteHomepageTemplate.as_view(),name="url_homepage_template"), )
模板表单代码
<ul class="dropdown-menu" role="menu" id="language-list"> <li class="" id="language-switcher"> <form action="{% url 'set_language' %}" method="post" name="lang_form"> {% csrf_token %} <input name="next" type="hidden" value="/" /> <select class="selectpicker" id="select_pickr" name="language"> {% get_available_languages as LANGUAGES %} {% get_language_info_list for LANGUAGES as languages %} {% for language in languages %} <option value="{{ language.code }}" {% if language.code == LANGUAGE_CODE %} selected="selected" {%endif %} data-content='{{ language.code }}}'> {% if language.code == 'en' %} English {% else %} हिंदी {% endif %} </option> {% endfor %} </select> </form> </li> </ul>
安全配置
DEBUG = False MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.middleware.locale.LocaleMiddleware',# for multi language 'django.contrib.sessions.middleware.SessionMiddleware', # ...caching 'django.middleware.cache.UpdateCacheMiddleware', 'corsheaders.middleware.CorsMiddleware', # new 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', # ...caching # 'django.middleware.cache.FetchFromCacheMiddleware', 'csp.middleware.CSPMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'debug_toolbar.middleware.DebugToolbarMiddleware', 'silk.middleware.SilkyMiddleware', 'axes.middleware.AxesMiddleware', ] AUTHENTICATION_BACKENDS = [ # AxesStandaloneBackend should be the first backend in the AUTHENTICATION_BACKENDS list. 'axes.backends.AxesStandaloneBackend', # Django ModelBackend is the default authentication backend. 'django.contrib.auth.backends.ModelBackend', ] X_FRAME_OPTIONS = 'DENY' SECURE_CONTENT_TYPE_NOSNIFF = True SECURE_HSTS_SECONDS = 86400 SECURE_HSTS_INCLUDE_SUBDOMAINS = True SECURE_HSTS_PRELOAD = True CSRF_TRUSTED_ORIGINS = ['https://*.mywebsite.com'] SECURE_BROWSER_XSS_FILTER = True SESSION_COOKIE_HTTPONLY = True CSP_DEFAULT_SRC = ("'self'","mywebsite.com","ajax.googleapis.com" ) CSP_SCRIPT_SRC = ( "'self'", "https://www.googletagmanager.com", "fonts.googleapis.com", "mywebsite.com", 'https://www.google.com/recaptcha/', 'https://www.gstatic.com/recaptcha/', 'https://www.youtube.com/embed/', 'https://cdnjs.cloudflare.com/ajax/libs/tiny-slider/2.9.2/min/tiny-slider.js', "https://ajax.googleapis.com/ajax/libs/jquery/1.12.0/jquery.min.js", # "'strict-dynamic'" # "'unsafe-inline'" ) CSP_FONT_SRC = ("'self'", "fonts.googleapis.com","fonts.gstatic.com") CSP_STYLE_SRC = [ "'self'", "mywebsite.com", "fonts.googleapis.com", "'unsafe-inline'" ] CSP_IMG_SRC = ("'self'",'mywebsite.com','data:','blob:') CSP_FRAME_SRC =( "'self'", 'https://www.google.com/recaptcha/', 'https://recaptcha.google.com/recaptcha/', 'https://www.youtube.com/', "https://www.google.com/maps/" ) CORS_ALLOWED_ORIGINS = [ "https://mywebsite.com" , ] #CONTENT SECURITY POLICY CSP_CONNECT_SRC =("'self'","https://www.google-analytics.com/") # CSP_OBJECT_SRC = ("'self'", 'mywebsite.com') CSP_BASE_URI = ("'self'", ) CSP_FRAME_ANCESTORS = ("'self'","https://www.google.com/" ) CSP_FORM_ACTION = ("'self'", ) CSP_MANIFEST_SRC = ("'self'", ) CSP_WORKER_SRC = ("'self'", ) CSP_MEDIA_SRC = ("'self'", ) CSP_CHILD_SRC = ("'self'", ) CSP_FORM_ACTION = ("'self'","mywebsite.com") # CSP_STYLE_SRC = ("'self'","mywebsite.com",) CSP_INCLUDE_NONCE_IN = [ 'default-src', 'script-src', # 'style-src', ] CSP_OBJECT_SRC = ("'self'","mywebsite.com")
解决方案
1. 修正中间件顺序
LocaleMiddleware的位置是核心问题,Django官方要求它必须放在SessionMiddleware之后、CommonMiddleware之前。当前配置中LocaleMiddleware在SessionMiddleware之前,会导致首次请求时session未初始化,无法正确生成并关联CSRF和语言cookie。修改后的中间件顺序:
MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.locale.LocaleMiddleware', # 调整到SessionMiddleware之后 'django.middleware.cache.UpdateCacheMiddleware', 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', # ...其余中间件保持不变 ]
2. 主动触发CSRF Cookie设置
首次请求时,即使模板包含{% csrf_token %},也可能因缓存或CSP限制导致CSRF cookie未及时写入。可以通过两种方式解决:
- 前端方式:在页面底部添加JS,页面加载时主动触发HEAD请求,强制设置CSRF cookie(需确保CSP允许内联脚本,或添加对应nonce):
document.addEventListener('DOMContentLoaded', () => { fetch(window.location.href, { method: 'HEAD', credentials: 'include' }); });
- 后端方式:在首页视图中主动生成CSRF token,确保cookie在首次请求时被设置:
from django.middleware.csrf import get_token from django.views.generic import TemplateView class WebsiteHomepageTemplate(TemplateView): def get(self, request, *args, **kwargs): get_token(request) # 主动生成CSRF token并设置cookie return super().get(request, *args, **kwargs)
3. 验证HTTPS环境下的Cookie设置
由于网站使用HTTPS,需确保session和CSRF cookie仅通过HTTPS传输,添加以下配置:
SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True
4. 检查CSP对表单提交的影响
确认CSP_FORM_ACTION包含/i18n/setlang/的来源,当前配置("'self'","mywebsite.com")已满足,但如果使用JS提交表单,需确保CSP_SCRIPT_SRC允许对应的脚本执行。
内容的提问来源于stack exchange,提问作者Abhi
相关产品推荐
相关产品推荐

