Як відправити токен CSRF за допомогою Django API та Flutter-web фронтенду? HeaderDisallowedByPreflightResponse [дублікат]

У мене є python/django web API з однією кінцевою точкою, назвемо її /api/v1/form. Цей API викликається з фронтенд-додатку Flutter-web. Наразі я використовую наступну конфігурацію, яка відключає перевірку токенів CSRF, і вона працює :

requirements.txt

Django==5.1.7
django-cors-headers==4.7.0

webserver/settings.py

...

ALLOWED_HOSTS = ["localhost"]
CORS_ALLOWED_ORIGINS = ["http://localhost:8001"] # flutter dev port

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'corsheaders',
    'webserver',
]

...

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'corsheaders.middleware.CorsMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

...

webserver/urls.py

from django.urls import path
import webserver.views

urlpatterns = [
    path('api/v1/form', webserver.views.api_v1_form, name="api_v1_form"),
]

...

webserver/views.py

from django.http import HttpResponse, HttpResponseBadRequest

def api_v1_form(request):
  if request.method == "POST":
    process_request(request.body)
    return HttpResponse("request successfully processed.")
  return HttpResponseBadRequest("Expected a POST request")

flutter/lib/page_form.dart

Future<int> sendForm(MyData data) async {
  final response = await http.post(
    Uri.parse("http://localhost:8000/api/v1/form"), 
    body: data.toString(),
  );

  return response.statusCode;
}

Ось що я не розумію: якщо я відключаю пакет CORS, щоб просто використовувати ванільний сервер Django, то виявляється, що я можу надсилати запити до API, але не можу отримати відповідь. Чому так відбувається ?

?

Нижче наведено конфігурацію, яка використовується для отримання токену CSRF та його використання у запитах.

settings.py

ALLOWED_HOSTS = ["localhost"]

CSRF_TRUSTED_ORIGINS = ["http://localhost:8001", "http://localhost:8000"]

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'webserver',
]

MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

Змінивши лише settings.py, я отримую відповідь 403 forbidden.

views.py

from django.views.decorators.csrf import ensure_csrf_cookie

@ensure_csrf_cookie
def api_v1_form(request):
  ... # unchanged

Відповідь все ще 403 forbidden

Нарешті, я спробував додати API /api/v1/token для відправки токена і повторного використання його в запиті.

views.py

def api_v1_token(request):
    if request.method == "GET":
        return HttpResponse(get_token(request))
    return HttpResponseBadRequest()

@ensure_csrf_cookie
def api_v1_form(request):
  if request.method == "OPTIONS": # POST -> OPTIONS
    # ...

page_form.dart

Future<int> envoyerFormulaire(MyData data) async {
  final tokenResponse = await http.get(
    Uri.parse("$apiUrlBase/token"), 
  );
  final token = tokenResponse.body.toString();

  Uri uriPost = Uri.parse("$apiUrlBase/form");
  final dataResponse = await http.post(
    uriPost, 
    body: data.toString(),
    headers: {
      "X-CSRFToken": token,
    }
  );

  return dataResponse.statusCode;
}

Проте, використовуючи інспектор, я отримую помилку, яка вказує на відсутність заголовка Access-Control-Allow-Origin при спробі отримати токен. Тому я додаю

views.py

def api_v1_token(request):
    if request.method == "GET":
        response = HttpResponse(get_token(request))
        response["Access-Control-Allow-Origin"] = "http://localhost:8001"
        return response
    return HttpResponseBadRequest()

def api_v1_form(request: HttpResponse):
  if  request.method == "OPTIONS":
    # ...
    response["Access-Control-Allow-Origin"] = "http://localhost:8001"
    return response

Тепер я можу отримати токен, але POST запит видає помилку PreflightMissingAllowOriginHeader. Я розумію, що я повинен додати заголовок, але хіба я не додав Access-Control-Allow-Origin до всіх запитів ?

Більше того, я бачу, що для POST запиту є два запити: preflight і fetch, preflight повертає помилку 500 internal, тому що django намагається розібрати вміст порожнього запиту. Це можна виправити, додавши фільтр

views.py

if request.method == "OPTIONS": # preflight
    res = HttpResponse()
    res["Access-Control-Allow-Origin"] = "http://localhost:8001"
    return res

if request.method == "GET":
  # normal processing after that

А тепер передпольотна перевірка повертає 200 OK. Але я отримую помилку CORS на запит вибірки (HeaderDisallowedByPreflightResponse).

Тепер я застряг. Здається, я чогось не розумію в заголовках і запитах CORS: мені не вистачає заголовків? з якими значеннями? і чи очікується, що програма обробить дані з попереднього польоту і поверне результат у наступному GET-запиті замість одного POST-запиту? Чи є це очікуваною поведінкою ?

?
Вернуться на верх