Djangoで学ぶCORS入門

※ 当サイトではアフィリエイト広告を利用しています。リンクは広告リンクも含みます。

この記事は約11分で読めます。

Web開発において「セキュリティ」と「利便性」はしばしば衝突します。特に、フロントエンドとバックエンドが異なるオリジンで動作する現代的な構成では、CORS(オリジン間リソース共有)の理解が不可欠です。

この記事では、CORSとは何か、オリジンの定義、CSRFトークンやセッションID、JWTなどの関連技術について、Djangoを使って実践的に学んでいきます。Zenn本の一部をベースに、解説しています。

内容には誤りが含まれている可能性もあるため、参考程度に読みつつ、必要に応じてご自身でも調査してみてください。なお、実行環境はWindows 11、Pythonが必要です。
詳しい内容は、こちらを参照してください。ローカルで実験できるDjangoとフロントのhtmlコードを載せています。

Djangoで学ぶCORS入門
Web開発において「セキュリティ」と「利便性」はしばしば衝突します。特に、フロントエンドとバックエンドが異なるオリジンで動作する現代的な構成では、CORS(オリジン間リソース共有)の理解が不可欠です。この本では、CORSとは何か、オリジンの...
広告
MINISFORUM日本公式ストア

 Webの安全を守る「門番」と、特別な「通行許可証」の話

Webブラウザには、ユーザーを悪意のあるサイトから守るための、とても重要なセキュリティルールがあります。それが同一オリジンポリシー (Same-Origin Policy) です。

同一オリジンポリシーとは?

これは、「あるウェブサイトから読み込まれたスクリプトは、そのサイトと同じ場所(オリジン)からしかリソース(データ)を取得できない」という、ブラウザの基本的なルールです。

ここで言うオリジンとは、URLの以下の3つの組み合わせを指します。

  1. プロトコル (http://https://)
  2. ホスト (example.comsub.example.com) 
    補足:サブドメインが違うだけでも「異なるオリジン」として扱われ、CORS制約の対象になります。
  3. ポート番号 (:80:443など)

このルールのおかげで、もしあなたが悪意のあるサイトを偶然開いてしまっても、そのサイトがあなたのネットバンキングやSNSに勝手にリクエストを送って情報を盗む、といったことを防いでくれます。まさに、ブラウザという家を守る「門番」のような存在です。


でも、最近のWeb開発では困ったことが…

しかし、最近のWebアプリケーション開発では、意図的にオリジンを分けることが一般的です。

  • ユーザーが見る画面(フロントエンド)はReactで作り、https://myfrontend.example.comで公開
  • データを処理する裏側(バックエンド)はDjangoで作り、https://api.example.comで公開

このように役割を分けると、開発や管理がしやすくなるという大きなメリットがあります。
ですが、この構成は「同一オリジンポリシー」のルールに反してしまいます。フロントエンドのスクリプトは、自分とは違うオリジンにあるAPIサーバーにデータをリクエストする必要があるからです。


そこで登場するのがCORS! 🤝

この問題を安全に解決するための仕組みが CORS (Cross-Origin Resource Sharing / オリジン間リソース共有) です。

CORSは、サーバー側が「このオリジンからなら、データを渡しても安全ですよ」という通行許可証 (Access-Control-Allow-Originヘッダー) を発行し、ブラウザの門番に伝える仕組みです。

この許可証があれば、ブラウザは安心して異なるオリジン間の通信を許可してくれます。

では、このCORSの仕組みがないと、具体的にどのような問題が起きるのでしょうか。次に、ReactとDjangoの例を見ていきましょう。


なぜCORSエラーは起こるの? 🤔

Web開発でよく遭遇するCORSエラー。その原因は、クライアントとサーバーではなく、ブラウザのセキュリティ機能にあります。以下の図で流れを追いましょう。

登場人物紹介

クライアント (Client)サーバー (Server)
🖥️ Reactアプリ🗄️ Django API
https://myfrontend.example.comhttps://api.example.com
ユーザーのブラウザ上で動く別の場所で動いている

myfrontendapiというサブドメインが違うため、これらは「異なるオリジン(Origin)」と見なされます。


通信の流れとエラー発生の瞬間 🚦

1. ユーザーがサイトにアクセス

  • ユーザーがブラウザで https://myfrontend.example.com を開きます。
  • Reactアプリがブラウザにダウンロードされ、実行が始まります。
  • [ユーザー] 👨‍💻 ---> [Reactサーバー] 🖥️

2. ReactアプリがAPIにデータを要求

  • ブラウザで実行されているJavaScript(React)が、fetchを使ってhttps://api.example.com/dataに「データをください!」とリクエストを送ります。
  • [ブラウザ内のReact] 📜 ---> [Django API] 🗄️

3. サーバーが応答(しかし…)

  • Django APIはリクエストを受け取り、「はい、これがデータです」と応答(レスポンス)を返します。
  • しかし、CORS設定がないため、応答ヘッダーに「myfrontend.example.comからのアクセスを許可しますよ」というAccess-Control-Allow-Originが含まれていません。
  • [ブラウザ内のReact] 📜 <--- [Django API] 🗄️ (許可証なしの返事)

4. ブラウザがブロック! 🛡️

  • データを受け取ったブラウザは、JavaScriptに渡す前にセキュリティチェックを行います。
  • 「おっと!リクエスト元のオリジン (myfrontend)と、応答元のオリジン (api)が違う。しかも、サーバーからの許可証 (Access-Control-Allow-Originヘッダー) がない! これはセキュリティ上危険かもしれないので、この応答はJavaScriptには渡せません!」
  • [ブラウザ] 🛡️ 💥 [JavaScript]

5. 結果:CORSエラー

  • ブラウザはJavaScriptにデータを渡すのをやめ、コンソールにCORSエラーを表示します。Reactアプリはデータを受け取れず、処理が失敗します。

実験サイト

フロント:https://kikuichige.com/cors_project/index.html(オリジンkikuichige.com)
バックエンド:https://django6.kikuichige.com/cors_project(オリジンdjango6.kikuichige.com)
バックエンドにサブドメインを使用していますので、同一オリジンではなくなります。
このままではフロントからバックエンドにアクセスできません。
そこでCORS (Cross-Origin Resource Sharing / オリジン間リソース共有) にします。

CORSは、サーバー側が「このオリジンからなら、データを渡しても安全ですよ」という通行許可証 (Access-Control-Allow-Originヘッダー) を発行し、ブラウザの門番に伝える仕組みです。
フロントにアクセスして実験してみてください。
htmlに結果を表示するようになっていますが、分かりにくいです。
メインは、開発者ツールのネットワークタブで通信内容を確認しながら実験してください。

注意
index.htmlでデバッグ用に表示させるためJavascriptが本来アクセスできないヘッダをaccess-control-expose-headersによりアクセスできるようにDjangoの設定をしています。本番では不要の設定です。
実際のレスポンス

access-control-expose-headers: Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Allow-Credentials, Access-Control-Max-Age

基本的なGETリクエスト

フロントからバックエンドにGETリクエストを飛ばします。
「GET /api/hello/」ボタンをクリックしてください。
CORSは正常に動作します。

レスポンスにaccess-control-allow-origin https://kikuichige.comがあるため成功します。
このヘッダを返すようにDjangoで処理しています。

POSTリクエスト (JSON)-Preflight Requestの確認

Preflight Requestとは

ブラウザが以下の条件で自動的に送信する 事前確認リクエスト です:

  • 単純でないHTTPメソッド: PUT, DELETE, PATCH など
  • カスタムヘッダー: Authorization, Content-Type: application/json など
  • 認証情報付きリクエスト: Cookieや認証ヘッダー

「POST /api/users/」ボタンをクリックしてください。

確認ポイント:(開発者ツールのネットワークタブで確認)
Content-Type: application/json を使ったPOSTリクエストなのでpreflightが発生します。

OPTIONS /api/users/  ← Preflightリクエスト
POST /api/users/     ← 実際のリクエスト

認証付きリクエストの実験(JWT認証テスト)

JWT(JSON Web Token)とは、

ユーザーの認証情報や属性情報を安全にやり取りするためのトークン形式です。
WebアプリケーションやAPIで広く使われていて、セッションレスな認証を実現する手段として人気があります。
JWTのポイント(HS256:共通鍵方式のJWT署名の場合)
・JWTはサーバーが生成する。
・ヘッダーとペイロード(ユーザー情報など)をBase64URLエンコードし、秘密鍵と指定された署名アルゴリズムでハッシュ値(署名)を生成して添付する。(JWTトークン)
・秘密鍵はサーバー側で安全に保管される。
・クライアントから送られてきたJWTが改ざんされていないかを検証するには、ヘッダーとペイロードを再度エンコードし、秘密鍵とアルゴリズムで署名を再計算し、添付された署名と一致するかを比較する。

「🎫 デモ用JWTトークン取得」をクリック

Content-Type: application/json を使ったPOSTリクエストなのでpreflightが発生します。
DjangoのレスポンスをHTMLが自動でJWT Token:を入力欄に表示します。

「GET /api/protected/」をクリック

JWTを送ってユーザー情報を引き出してみます。
DjangoがJWT検証を行い。JWTに入っているuser情報を抜き出しレスポスします。

認証付きリクエストの実験(Cookie認証テスト)

Cookie、セッションid、csrftokenを理解することが目的です。

「ログイン」ボタンをクリック

「🍪 Cookie認証リクエスト」セクションで「ログイン」ボタンをクリック
ログインボタンをクリックすると、以下の処理が実行されます。
・同一オリジンではないためAPIによりCSRFトークン取得します。CSRFトークンがないとPOSTでログイン情報が送れません。
bodyで送られてくるcsrfトークンは、cookieに保存されるものとは違いマスクされたものになります。
・続いてcsrfトークン、user、passwordのログインのリクエストが行われます。
・Djangoでcsrfトークンの検証を行い。ログイン処理と成功のレスポンスが返ってきます。
・また、セッションidが発行されクッキーに保存します。

「保護されたデータ取得」で認証状態を確認

「保護されたデータ取得」ボタンをクリック
・セッションidとcsrfトークンをつけてリクエストします。
・Djangoで検証され、登録されているusernameがレスポンスされます。

「ログアウト」ボタンをクリック

「ログアウト」ボタンをクリックするとログアウト用のAPIにアクセスします。
ログアウトに成功するとDjangoから空のセッションidがセットされログアウトが完了します。

所感

複数のAI(ChatGPT、Claude、Copilot、Gemini)を活用しながら執筆を進めましたが、それぞれの回答に違いがあり、情報の取捨選択には苦労しました。最終的には、コードベースで実際に動作を確認しながら理解を深めるアプローチが最も有効でした。

CORSや認証の仕組みは、理論だけでは掴みにくい部分も多く、実験を通じて「なぜそうなるのか」「どうすれば回避できるのか」が明確になっていきます。特に、DjangoとReactなどの組み合わせによるオリジン間通信の挙動は、開発現場で頻繁に直面する課題であり、実践的な知識として役立つと感じました。

このページが、同じように悩む開発者のヒントになれば嬉しいです。

この記事を書いたイチゲを応援する(質問でもokです)
Vプリカでのお支払いがおすすめです。

MENTAやってます(ichige)

タイトルとURLをコピーしました