Fetch API と Django のクロスオリジンで気をつける事

個人的に開発する中で、localhost:8080 でホストしている javascript ファイルから localhost:8000 でホストしてる Django に対し HTTP リクエストをかけたところ、CORS (Cross Origin Resource Sharing) 関連のエラーが色々出ました。

色々調べながら解決しましたが、自分なりに分かりやすく整理したかったのでこの記事にまとめました。

Javascript からの HTTP リクエスト には fetch API を使っています。

  1. CORS (Cross Origin Resource Sharing) とは
  2. Step 1: GET
  3. Step 2: request に Cookie を含める
  4. Step 3: POST や DELETE

CORS (Cross Origin Resource Sharing) とは

下記こちらからの抜粋です。

アプリケーションが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンからの場合は正しい CORS ヘッダーを含んでいることが必要です。

オリジン間リソース共有の仕様は、ウェブブラウザーから情報を読み取ることを許可されているオリジンをサーバーが記述することができる、新たな HTTP ヘッダーを追加することで作用します。

Step 1: GET

まずは一番単純な GET から。Cookie も送らない想定です。

何もクロスオリジン対応をしていないと恐らく「No 'Access-Control-Allow-Origin' header is present on the requested resource.」という様なエラーが発生します。

クロスオリジンの request に対してサーバー側から Access-Control-Allow-Origin ヘッダーを適切に返す必要があります。

Fetch API での対応

Fetch API は普通に fetch() で request URL を叩けば大丈夫です。

let fetchResponse = await fetch(
    'http://localhost:8000/api/something'
)

メインはサーバー側(Django)での対応です。

Django での対応

django-cors-headers を pip install し、settings.py を下記のように追記して http://localhost:8080 からのリクエストを許可します。

pip install django-cors-headers
# settings.py

INSTALLED_APPS = [
   ...
   'corsheaders',
]

MIDDLEWARE = [
  ...
  'corsheaders.middleware.CorsMiddleware',
]

CORS_ALLOWED_ORIGINS = ['http://localhost:8080']

これでクロスオリジンの request を送れる様になります。(localhost:8080 → localhost:8000)

次に、request に cookie を含める設定を行います。

クロスオリジンでの fetch API は「デフォルトでは cookies を送信しない」仕様になっています。

これを解決するには fetch API の credentials パラメータを 'include' に設定し、サーバー側は Access-Control-Allow-Credentials ヘッダーを true で返す必要があります。

Fetch API での対応

fetch API のオプションに credentials: 'include' を含めます。

let fetchResponse = await fetch(
    'http://localhost:8000/api/something',
    {credentials: 'include'} // 追記分
)

Django での対応

settings.py に「CORS_ALLOW_CREDENTIALS = True」を追記します。

# settings.py
CORS_ALLOW_CREDENTIALS = True

これで Fetch API で Cookie が送れる様になり、Django 側もそれを受けられる様になりました。

Django でログイン済みのはずが AnonymousUser となる件

ちなみにログインユーザーを対象にした処理を Django 側で行う場合、上記の設定をしていないと、例えブラウザの cookie と Django の django_session テーブルで sessionid が一致していても 500 Internal Server Error や 403 Forbidden が発生すると思います。

理由は、クライアント側からの sessionid を request.COOKIES として受け取れていないため、request.user が非ログインユーザーを意味する AnonymousUser となるためです。

Step 3: POST や DELETE

さらに、クロスオリジンで POST や DELETE など unsafe な request を送る場合、クライアント側の fetch() に X-CSRFToken ヘッダーを設定し、Django 側では settings.py の CSRF_TRUSTED_ORIGINS のリストに request を許可するオリジンを記述する必要がります。

Fetch API での対応

let fetchResponse = await fetch(
    'http://localhost:8000/api/something',
    {credentials: 'include'},
    // ここから追記分
    method: 'POST',
    headers: {
        'X-CSRFToken': csrftoken, // Cookie から取得
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({
        'someKey1': someData1,
        'someKey2': someData2
    }
)

GET メソッドは fetch のデフォルト値でしたが、POST や他のメソッドを使用する場合は method を指定する必要があります。

また、headers に X-CSRFToken として cookie から取得した csrftoken をアサインし、Content-Type も指定する必要があります。

Django での対応

settings.py の CSRF_TRUSTED_ORIGINS のリストに request を許可するオリジンを記述します。

# settings.py
CSRF_TRUSTED_ORIGINS = ['http://localhost:8080']