django-cors-headers не работает, когда включен i18n
У меня есть рабочая среда с:
django==1.10
django-rest-framework==3.5.3
djangorestframework-jsonapi==2.1.1
channels
(самый последний)daphne
(последний) вместоgunicorn
,
я использую nginx
как прокси сервер выше daphne
внутри докера
Я строю отдельный angular 2
SPA, который подключается к вышеупомянутому бэкэнду, и я использую django-cors-headers==2.0.2
разрешить подключения из этого веб-приложения.
Работает с: USE_I18N = False
Он отлично работает, когда я установил Django USE_I18N = False
, При попытке аутентификации в бэкэнде я отправляю запрос POST, эквивалентный следующему:
curl -H "Content-Type: application/vnd.api+json" -X POST -d '{"data": {"type": "obtainJSONWebTokens", "attributes": {"email":"admin@email.com", "password":"password"}}}' http://localhost/api/auth/login/ --verbose
Вывод из curl:
* Trying ::1...
* Connected to localhost (::1) port 80 (#0)
> POST /api/auth/login/ HTTP/1.1
> Host: localhost
> User-Agent: curl/7.49.0
> Accept: */*
> Content-Type: application/vnd.api+json
> Content-Length: 107
>
* upload completely sent off: 107 out of 107 bytes
< HTTP/1.1 200 OK
< Server: nginx/1.11.9
< Date: Mon, 20 Mar 2017 13:00:47 GMT
< Content-Type: application/vnd.api+json
< Transfer-Encoding: chunked
< Connection: keep-alive
< Allow: POST, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Content-Language: en
< Vary: Accept, Accept-Language, Cookie
<
{"data":{"token":"<token>"}}
* Connection #0 to host localhost left intact
Я получаю токен JWT, который должен получить. Все работает отлично.
Сбой с: USE_I18N = True
Тем не менее, то же соединение не удается, когда USE_I18N = True
,
Вывод из curl:
* Trying ::1...
* Connected to localhost (::1) port 80 (#0)
> POST /api/auth/login/ HTTP/1.1
> Host: localhost
> User-Agent: curl/7.49.0
> Accept: */*
> Content-Type: application/vnd.api+json
> Content-Length: 107
* upload completely sent off: 107 out of 107 bytes
< HTTP/1.1 302 Found
< Server: nginx/1.11.9
< Date: Mon, 20 Mar 2017 12:53:49 GMT
< Content-Type: text/html; charset=utf-8
< Transfer-Encoding: chunked
< Connection: keep-alive
< Location: /en/api/auth/login/
< Vary: Cookie
<
* Connection #0 to host localhost left intact
Возвращенная ошибка на стороне клиента:
XMLHttpRequest cannot load http://localhost/api/auth/login/. Redirect from 'http://localhost/api/auth/login/' to 'http://localhost/en/api/auth/login/' has been blocked by CORS policy: Request requires preflight, which is disallowed to follow cross-origin redirect.
Соответствующие настройки:
INSTALLED_APPS += (
'corsheaders',
)
if DEBUG is True:
CORS_ORIGIN_ALLOW_ALL = True
MIDDLEWARE_CLASSES = (
'corsheaders.middleware.CorsMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.admindocs.middleware.XViewMiddleware',
)
Казалось бы, это не запрос клиента, но сбой, а перенаправление с " http://localhost/api/auth/login/" на " http://localhost/en/api/auth/login/", где Django добавляет 'en' к URL.
Может кто-то пролить свет на это?
Я искал django-cors-headers
связанные с этим проблемы, но ни одна из них не относится к этой явной несовместимости с I18N. Библиотека прекрасно работает без I18N, только не с включенным.
РЕДАКТИРОВАТЬ 2017-03-21
Учитывая ограничения, указанные в принятом ответе, я решил просто избегать перенаправления URL-адреса языка Django. При использовании USE_I18N = True
Я полностью избежал i18n_patterns
в корневом URLconf.
Фактически, Django Rest Framework заявляет, что это лучшая практика для клиентов API:
Если вы хотите разрешить языковые предпочтения для каждого запроса, вам необходимо включить
django.middleware.locale.LocaleMiddleware
в вашемMIDDLEWARE_CLASSES
установка.Вы можете найти больше информации о том, как языковые предпочтения определены в документации Django. Для справки, метод:
- Сначала он ищет языковой префикс в запрошенном URL.
- В противном случае, он ищет
LANGUAGE_SESSION_KEY
введите текущий сеанс пользователя.- В противном случае он ищет печенье.
- В противном случае это смотрит на
Accept-Language
HTTP заголовок.- В противном случае он использует глобальный
LANGUAGE_CODE
установка.Для клиентов API наиболее подходящим из них обычно будет использование
Accept-Language
заголовок; Сеансы и файлы cookie не будут доступны, если не используется аутентификация сеанса, и, как правило, лучше использоватьAccept-Language
заголовок для клиентов API вместо использования языковых префиксов URL.
Итак, я сохранил вышеупомянутые настройки, но изменил в корне следующее URLconf
:
urlpatterns += i18n_patterns(
url(_(r'^api/$'), SwaggerSchemaView.as_view(), name='api'),
url(_(r'^api/account/'), include(account_patterns, namespace='account')),
url(_(r'^api/auth/'), include(auth_patterns, namespace='auth')),
url(_(r'^api/'), include('apps.party.api.urls', namespace='parties')),
url(_(r'^api/'), include('apps.i18n.api.urls', namespace='i18n')),
url(_(r'^api-auth/'), include('rest_framework.urls', namespace='rest_framework')),
url(_(r'^admin/'), include(admin_patterns)),
url(_(r'^docs/'), include('apps.docs.urls'))
)
в
urlpatterns += ([
url(_(r'^api/$'), SwaggerSchemaView.as_view(), name='api'),
url(_(r'^api/account/'), include(account_patterns, namespace='account')),
url(_(r'^api/auth/'), include(auth_patterns, namespace='auth')),
url(_(r'^api/'), include('apps.party.api.urls', namespace='parties')),
url(_(r'^api/'), include('apps.i18n.api.urls', namespace='i18n')),
url(_(r'^api-auth/'), include('rest_framework.urls', namespace='rest_framework')),
url(_(r'^admin/'), include(admin_patterns)),
url(_(r'^docs/'), include('apps.docs.urls'))]
)
Итак, сейчас занимаемся:
curl -H "Content-Type: application/vnd.api+json" -H "Accept-Language: pt" -X POST -d '{"data": {"type": "obtainJSONWebTokens", "attributes": {"email":"admin@email.com", "password":"password"}}}' http://localhost:8000/api/auth/login/ --verbose
возвращает ожидаемый ответ на запрошенном языке (обратите внимание на включение "Accept-Language: pt"
в запросе выше):
* Trying ::1...
* Connected to localhost (::1) port 8000 (#0)
> POST /api/auth/login/ HTTP/1.1
> Host: localhost:8000
> User-Agent: curl/7.49.0
> Accept: */*
> Content-Type: application/vnd.api+json
> Accept-Language: pt
> Content-Length: 107
>
* upload completely sent off: 107 out of 107 bytes
< HTTP/1.1 200 OK
< Transfer-Encoding: chunked
< Allow: POST, OPTIONS
< X-Frame-Options: SAMEORIGIN
< Vary: Accept, Accept-Language, Cookie
< Content-Language: pt
< Content-Type: application/vnd.api+json
<
{"data": {"token":"<token>"}}
* Connection #0 to host localhost left intact
2 ответа
По сути, вы столкнулись с ошибкой в более старой версии стандарта CORS.
Первоначальный стандарт в основном делал невозможным локальное перенаправление при использовании предварительных запросов. См. Этот вопрос по теме, а также отчет об ошибках в стандарте Fetch.
В вашем случае это происходит с USE_I18N = True
потому что этот параметр вызывает перенаправления.
Надеюсь, это исправление скоро будет реализовано браузерами. (Согласно последнему отчету об ошибке Fetch, она уже работает в Edge.) Тем временем этот ответ предлагает некоторые обходные пути.
У меня была такая же проблема, так как я не использовал i18n_patterns
для всех моих URL и одного из URL, который не был внутри i18n_patterns
вернул 404 ответ. Я решил эту проблему, переписав Middleware LocaleMiddleware, который по умолчанию импортирует Django.
class CustomLocaleMiddleware(LocaleMiddleware):
def current_urlpattern_is_locale(self, path):
try:
resolver = get_resolver(None).resolve(path)
except Resolver404:
return self.is_language_prefix_patterns_used()
return isinstance(resolver, LocaleRegexURLResolver)
def process_response(self, request, response):
language = translation.get_language()
language_from_path = translation.get_language_from_path(request.path_info)
if (response.status_code == 404 and not language_from_path
and self.current_urlpattern_is_locale(request.path)):
urlconf = getattr(request, 'urlconf', None)
language_path = '/%s%s' % (language, request.path_info)
path_valid = is_valid_path(language_path, urlconf)
if (not path_valid and settings.APPEND_SLASH
and not language_path.endswith('/')):
path_valid = is_valid_path("%s/" % language_path, urlconf)
if path_valid:
script_prefix = get_script_prefix()
language_url = "%s://%s%s" % (
request.scheme,
request.get_host(),
# insert language after the script prefix and before the
# rest of the URL
request.get_full_path().replace(
script_prefix,
'%s%s/' % (script_prefix, language),
1
)
)
return self.response_redirect_class(language_url)
if not (self.is_language_prefix_patterns_used()
and language_from_path):
patch_vary_headers(response, ('Accept-Language',))
if 'Content-Language' not in response:
response['Content-Language'] = language
return response