Flask-CORS徹底解説:Python FlaskアプリでCORSをマスターしよう! 🌐

Web開発

モダンなWeb開発、特にフロントエンド(React, Vue, Angularなど)とバックエンドAPI(Flask, Djangoなど)を分離して開発する構成では、しばしば「オリジン(Origin)」の違いによる問題に直面します。オリジンとは、URLのスキーム(http, https)、ホスト(ドメイン名)、ポート番号の組み合わせを指します。例えば、http://localhost:3000 で動作するフロントエンドアプリケーションが http://localhost:5000 で動作するFlask APIにアクセスしようとする場合、これらは異なるオリジンと見なされます。

セキュリティ上の理由から、Webブラウザは同一オリジンポリシー (Same-Origin Policy, SOP) という仕組みを採用しています。これは、あるオリジンから読み込まれた文書やスクリプトが、他のオリジンにあるリソースにアクセスすることを原則として制限するものです。これにより、悪意のあるサイトが勝手に他のサイトの情報を読み取ったり、操作したりすることを防いでいます。

しかし、正当な理由で異なるオリジン間でリソースを共有したいケースは多々あります(例:APIサーバーからデータを取得するSPA)。ここで登場するのが CORS (Cross-Origin Resource Sharing)、日本語では「オリジン間リソース共有」と呼ばれる仕組みです。CORSは、サーバーが特定のオリジンからのリクエストを許可するようにHTTPヘッダーを追加することで、ブラウザにクロスオリジンリクエストを許可させるための標準的なメカニズムです。

Pythonの軽量WebフレームワークであるFlaskでAPIを構築する場合、このCORSの設定を簡単に行うための拡張機能が Flask-CORS です。このライブラリを使うことで、Flaskアプリケーションは異なるオリジンで動作するフロントエンドアプリケーションからのリクエストを安全に受け入れることができるようになります。この記事では、Flask-CORSの基本的な使い方から詳細な設定オプション、セキュリティ上の注意点までを詳しく解説していきます。🚀

インストールと基本的な使い方 🛠️

Flask-CORSの導入は非常に簡単です。まず、pipを使ってインストールします。

pip install Flask-CORS

インストール後、FlaskアプリケーションにCORSサポートを追加する最も簡単な方法は、CORS クラスをインポートし、Flaskアプリケーションオブジェクトを渡してインスタンス化することです。これにより、すべてのルートで、すべてのオリジンからのリクエストがデフォルトで許可されます。

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
# これだけでアプリ全体に基本的なCORS設定が適用される
CORS(app)

@app.route("/")
def hello_world():
    return "Hello, cross-origin world! 👋"

if __name__ == '__main__':
    app.run(debug=True)

上記のコードでは、CORS(app) の一行を追加するだけで、異なるオリジン(例えば、ローカル開発中の http://localhost:3000 など)で動作するフロントエンドから http://localhost:5000(Flaskアプリがデフォルトで動作するポート)の / ルートへアクセスできるようになります。

注意: この最も簡単な設定(CORS(app))は、すべてのオリジン(*)からのアクセスを許可します。開発中は便利ですが、本番環境ではセキュリティリスクを高める可能性があるため、通常は推奨されません。本番環境では、許可するオリジンを明示的に指定することが重要です。次のセクションで、より詳細な設定方法を見ていきましょう。

詳細な設定オプション ⚙️

Flask-CORSは、様々なオプションを通じてCORSの挙動を細かく制御することができます。設定は主に以下の方法で行えます。

  • CORS() の初期化時または init_app() 呼び出し時にキーワード引数として渡す。
  • Flaskアプリケーションの設定 (app.config) を通じて行う (例: app.config['CORS_ORIGINS'] = [...])。
  • リソースレベルで設定する (パスごとに異なる設定を適用)。
  • ルートデコレータ @cross_origin() を使用して特定のルートに設定する。

設定の優先順位は、リソースレベル設定 > キーワード引数設定 > アプリケーションレベル設定 > デフォルト設定 の順になります。

主要な設定オプション

以下は、よく使われる設定オプションとその説明です。

オプション名 (引数/設定キー) 説明 デフォルト値 設定例 (CORS()引数)
origins / CORS_ORIGINS 許可するオリジンのリスト。文字列、正規表現、またはそれらのリストを指定可能。"*" は全てのオリジンを許可。 "*" origins=["http://localhost:3000", "https://example.com"]
methods / CORS_METHODS 許可するHTTPメソッドのリスト。 ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"] methods=["GET", "POST", "PUT"]
allow_headers / CORS_HEADERS 許可するリクエストヘッダーのリスト。クライアントが Access-Control-Request-Headers で指定したヘッダーのうち、ここで許可されたものが Access-Control-Allow-Headers レスポンスヘッダーに含まれる。 None (デフォルトで主要なヘッダーは許可される) allow_headers=["Content-Type", "Authorization", "X-Requested-With"]
expose_headers / CORS_EXPOSE_HEADERS ブラウザのJavaScriptからアクセス可能にすることを許可するレスポンスヘッダーのリスト。CORS仕様では、デフォルトで特定のヘッダー以外はスクリプトからアクセスできない。 None expose_headers=["X-My-Custom-Header", "X-Total-Count"]
supports_credentials / CORS_SUPPORTS_CREDENTIALS True に設定すると、クロスオリジンリクエストでCookieやAuthorizationヘッダーなどの認証情報(クレデンシャル)の送信を許可する。この場合、Access-Control-Allow-Origin にワイルドカード "*" を使用できなくなり、具体的なオリジンを指定する必要がある。レスポンスには Access-Control-Allow-Credentials: true ヘッダーが付与される。 False supports_credentials=True
max_age / CORS_MAX_AGE プリフライトリクエストの結果をブラウザがキャッシュできる最大時間(秒数、timedeltaオブジェクト、または文字列)。Access-Control-Max-Age ヘッダーとして返される。 None max_age=86400 (1日)
automatic_options / CORS_AUTOMATIC_OPTIONS (@cross_origin() デコレータのみ) Trueの場合、FlaskのデフォルトのOPTIONSメソッド処理を上書きし、CORSヘッダーを返すようにする。 True automatic_options=True (デコレータ引数)
resources (CORS() 初期化時のみ) パス(正規表現も可)と、そのパスに適用するオプションの辞書のマッピング。これにより、リソースごとに異なるCORSポリシーを設定できる。 {r"/*": {"origins": "*"}} resources={r"/api/*": {"origins": "https://api.example.com"}, r"/public/*": {"origins": "*"}}

設定例

1. 特定のオリジンのみを許可 (アプリケーション全体)

from flask import Flask
from flask_cors import CORS

app = Flask(__name__)
# http://localhost:3000 と https://myapp.com からのアクセスのみ許可
CORS(app, origins=["http://localhost:3000", "https://myapp.com"])

@app.route('/data')
def get_data():
    return {"message": "This data is accessible from specific origins."}

if __name__ == '__main__':
    app.run(port=5000)

2. 特定のルートのみにCORSを適用 (デコレータを使用)

@cross_origin() デコレータを使うと、特定のビュー関数に対して個別にCORS設定を適用できます。引数なしで使うとデフォルト設定(全オリジン許可など)が適用されます。

from flask import Flask, jsonify
from flask_cors import CORS, cross_origin

app = Flask(__name__)
# アプリ全体にはCORSを設定しないか、限定的な設定にする
# CORS(app) # ← コメントアウト または限定的な設定

@app.route('/public')
def public_route():
    return "This endpoint does not have CORS enabled by default."

@app.route('/api/special')
@cross_origin() # このルートのみデフォルトのCORS設定を適用 (全オリジン許可)
def special_api():
    return jsonify({"message": "This endpoint has default CORS enabled."})

@app.route('/api/restricted')
@cross_origin(origins="https://trusted.com", methods=["GET", "POST"], supports_credentials=True) # より詳細な設定
def restricted_api():
    # ここでは認証情報が必要な処理などを行う想定
    return jsonify({"message": "This endpoint allows trusted.com with credentials."})

if __name__ == '__main__':
    app.run(port=5000)

3. リソースベースの設定 (パスごとに異なる設定)

resources 引数を使うと、URLパスパターンに基づいて異なるCORSポリシーを定義できます。

from flask import Flask, jsonify
from flask_cors import CORS

app = Flask(__name__)

# パスごとにCORSポリシーを設定
# /api/v1/* は http://localhost:3000 からのみ許可
# /public/* は全てのオリジンからGETメソッドのみ許可
cors_config = {
    r"/api/v1/*": {"origins": "http://localhost:3000", "methods": ["GET", "POST", "PUT", "DELETE"]},
    r"/public/*": {"origins": "*", "methods": ["GET"]}
}
CORS(app, resources=cors_config)

@app.route('/api/v1/users')
def get_users():
    return jsonify([{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}])

@app.route('/public/info')
def get_public_info():
    return jsonify({"version": "1.0", "status": "OK"})

@app.route('/internal') # このルートはCORS設定の対象外
def internal_route():
    return "Internal use only."

if __name__ == '__main__':
    app.run(port=5000)

4. 認証情報 (Cookieなど) を扱う場合

フロントエンドがCookieやAuthorizationヘッダーを伴うリクエストを送信する場合 (fetchcredentials: 'include' や Axios の withCredentials: true など)、サーバー側で supports_credentials=True を設定する必要があります。この設定を行うと、origins"*" を指定できなくなるため、許可するオリジンを明示的にリストする必要があります。

from flask import Flask, jsonify, session, request
from flask_cors import CORS, cross_origin
import os

app = Flask(__name__)
app.secret_key = os.urandom(24) # sessionを使うために必要

# 認証情報を許可し、特定のオリジンを指定
CORS(app, origins=["http://localhost:3000", "https://secure-frontend.com"], supports_credentials=True)

@app.route('/login', methods=['POST'])
def login():
    # 実際の認証処理 (ここでは省略)
    username = request.json.get('username')
    if username == 'testuser':
        session['user_id'] = 1 # セッションCookieを設定
        return jsonify({"message": "Login successful"}), 200
    return jsonify({"message": "Invalid credentials"}), 401

@app.route('/profile')
def profile():
    if 'user_id' in session:
        # 認証済みユーザー向けのデータを返す
        return jsonify({"user_id": session['user_id'], "data": "Sensitive profile data"})
    else:
        return jsonify({"message": "Unauthorized"}), 401

@app.route('/logout', methods=['POST'])
def logout():
    session.pop('user_id', None) # セッションを削除
    return jsonify({"message": "Logout successful"}), 200

if __name__ == '__main__':
    app.run(port=5000, debug=True)

この場合、フロントエンド側でもリクエスト時に認証情報を含める設定が必要です。

ユースケースとシナリオ 💡

Flask-CORSが役立つ典型的なシナリオをいくつか見てみましょう。

  • シングルページアプリケーション (SPA) との連携: React, Vue, Angularなどで構築されたフロントエンドアプリケーションは、通常、FlaskなどのバックエンドAPIサーバーとは異なるオリジン(開発中は異なるポート、本番では異なるサブドメインなど)で提供されます。SPAがAPIからデータを取得したり、データを送信したりするには、バックエンドでCORSを適切に設定する必要があります。Flask-CORSを使えば、特定のフロントエンドオリジンからのリクエストのみを許可するように簡単に設定できます。
  • マイクロサービスアーキテクチャ: 複数の独立したサービス(マイクロサービス)が連携して動作するシステムでは、サービス間でAPI呼び出しが発生することがよくあります。これらのサービスが異なるドメインやポートでホストされている場合、サービス間の通信を可能にするためにCORSの設定が必要になることがあります。
  • サードパーティへのAPI提供: 自社のFlask APIを、外部の開発者やパートナー企業が彼らのWebアプリケーションから利用できるように公開する場合、それらの外部オリジンからのアクセスを許可するためにCORSの設定が不可欠です。origins オプションで許可するドメインを管理できます。
  • WebフォントやCDNリソース: 直接Flask-CORSの設定とは異なりますが、関連する概念として、Webフォントや他のCDN(コンテンツ配信ネットワーク)上のリソースを自身のWebページから利用する場合も、提供元サーバーで適切なCORSヘッダー(通常は Access-Control-Allow-Origin: *)が設定されている必要があります。

これらのシナリオにおいて、Flask-CORSは開発者が複雑なCORSヘッダーを手動で管理する手間を省き、Flaskアプリケーションに簡単かつ柔軟にクロスオリジンアクセス制御を組み込むことを可能にします。😊

セキュリティに関する考慮事項 🛡️

CORSは便利な機能ですが、設定を誤るとセキュリティリスクを生む可能性があります。以下の点に注意して、安全な設定を心がけましょう。

  • origins = "*" の使用は慎重に: すべてのオリジンからのアクセスを許可する origins="*" (またはデフォルト設定) は、最も簡単な設定ですが、セキュリティ的には最も緩い設定です。これにより、意図しないウェブサイトからのリクエストも受け付けてしまう可能性があります。特に、認証情報 (supports_credentials=True) を扱うAPIでは、"*" を使用すべきではありません(技術的にもブロックされます)。可能な限り、信頼できる特定のオリジンのみを明示的に指定するようにしましょう。
    # 良くない例 (本番環境では避けるべき)
    # CORS(app) や CORS(app, origins="*")
    
    # 良い例
    CORS(app, origins=["https://my-frontend.com", "https://partner-site.net"])
  • supports_credentials = True のリスク: このオプションを有効にすると、ブラウザはクロスオリジンリクエストにCookieなどの認証情報を含めて送信します。サーバーがこれらの情報に基づいて処理を行う場合、攻撃者がユーザーを騙して悪意のあるリクエストを送信させるCSRF (Cross-Site Request Forgery) 攻撃のリスクが高まる可能性があります。supports_credentials=True を使用する場合は、Flask-WTFや他の方法によるCSRF対策を必ず実装してください。また、許可するオリジンを厳密に制限することが不可欠です。
  • 許可するメソッドとヘッダーの制限: methodsallow_headers オプションを使用して、アプリケーションが必要とするHTTPメソッドとヘッダーのみを許可するように制限することも、セキュリティを向上させる一助となります。不要なメソッド(例:機密情報を変更しないAPIでDELETEを許可するなど)やヘッダーを許可しないようにしましょう。
  • プリフライトリクエストの理解: 後述するプリフライトリクエスト (OPTIONSメソッド) は、実際の複雑なリクエスト (PUT, DELETE, カスタムヘッダー付きなど) を送信する前に、サーバーがそのリクエストを許可するかどうかをブラウザが確認するためのものです。Flask-CORSはこのプリフライトリクエストに自動で応答しますが、その応答内容(許可されるメソッドやヘッダー)が適切であることを確認することが重要です。
  • ログのインジェクション脆弱性 (過去の事例): 過去にはFlask-CORSの特定のバージョン (例: CVE-2024-1681) で、デバッグログレベルが有効な場合に、リクエストパスに特殊文字 (CRLFシーケンス) を含めることでログ内容を操作できる脆弱性が報告されたことがあります (2024年)。常にライブラリを最新の状態に保ち、セキュリティ情報を確認することが重要です。

セキュリティは常にトレードオフです。利便性と安全性のバランスを取りながら、アプリケーションの要件に合わせて最も適切なCORS設定を選択することが求められます。🔒

高度なトピックとトラブルシューティング 🔍

プリフライトリクエスト (Preflight Requests)

特定の種類のクロスオリジンリクエスト(「単純リクエスト」ではないリクエスト)を送信する前に、ブラウザは自動的に「プリフライトリクエスト」と呼ばれる予備のリクエストをサーバーに送信します。これはHTTPの OPTIONS メソッドを使用します。

以下のような場合にプリフライトリクエストが発生します:

  • GET, HEAD, POST 以外のHTTPメソッド (PUT, DELETE, PATCH など) を使用する場合。
  • POST メソッドで、Content-Type ヘッダーの値が application/x-www-form-urlencoded, multipart/form-data, text/plain 以外の場合 (例: application/json)。
  • リクエストにカスタムヘッダー (例: X-Custom-Header) が含まれる場合。

プリフライトリクエストの目的は、サーバーに対して、実際のクロスオリジンリクエストで使用される予定のメソッドやヘッダーが許可されているかどうかを確認することです。ブラウザは Origin, Access-Control-Request-Method, Access-Control-Request-Headers といったヘッダーをOPTIONSリクエストに含めて送信します。

サーバー (Flask-CORS) はこのOPTIONSリクエストを受け取ると、設定に基づいて適切なCORSレスポンスヘッダー (Access-Control-Allow-Origin, Access-Control-Allow-Methods, Access-Control-Allow-Headers, Access-Control-Max-Age など) を返します。ブラウザはこのレスポンスを見て、実際の(例えばPUT)リクエストを送信しても安全かどうかを判断します。許可されていれば、ブラウザは続いて実際のリクエストを送信します。

Flask-CORSはデフォルトで (automatic_options=True の場合)、これらのプリフライトリクエストを自動的に処理し、Flaskアプリケーションのルート定義で明示的にOPTIONSメソッドを処理する必要はありません。ただし、automatic_options=False に設定した場合や、OPTIONSメソッドで独自の処理を行いたい場合は、自分でルートを定義する必要があります。

CORSエラーのデバッグ

開発中にCORSエラーに遭遇することはよくあります。「Access to fetch at ‘…’ from origin ‘…’ has been blocked by CORS policy: …」といったエラーメッセージがブラウザのコンソールに表示された場合、以下の手順でデバッグを試みてください。

  1. ブラウザの開発者ツールを確認する: ネットワークタブを開き、失敗したリクエストを選択します。
    • OPTIONSリクエスト (プリフライト) が存在するか?: もしあれば、そのリクエストとレスポンスヘッダーを確認します。サーバーからのレスポンスに期待する Access-Control-Allow-... ヘッダーが含まれているか、値は正しいか(特に Access-Control-Allow-Origin がリクエスト元のオリジンと一致しているか、または * か)を確認します。
    • 実際のリクエスト (GET, POSTなど): そのリクエストのヘッダー (特に Origin ヘッダー) と、サーバーからのレスポンスヘッダーを確認します。レスポンスに Access-Control-Allow-Origin ヘッダーが存在するか、値が正しいかを確認します。
    • コンソールのエラーメッセージ: エラーメッセージには、なぜブロックされたかの具体的な理由(例: “No ‘Access-Control-Allow-Origin’ header is present”, “Response to preflight request doesn’t pass access control check”, “Credentials flag is ‘true’, but the ‘Access-Control-Allow-Credentials’ header is ”.”, “Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.”)が示されていることが多いので、よく読みましょう。
  2. Flask-CORSの設定を確認する: Flaskアプリケーションのコードで、CORS()@cross_origin() の設定が正しいか再確認します。
    • origins は正しく設定されていますか? (タイポはないか? スキーム、ホスト、ポートは正しいか?)
    • methods に必要なメソッドが含まれていますか?
    • カスタムヘッダーを使用している場合、allow_headers に含まれていますか?
    • 認証情報 (Cookieなど) を扱っている場合、supports_credentials=True が設定され、かつ origins"*" でなく具体的なオリジンが指定されていますか?
    • デコレータとアプリケーション全体の設定が意図せず競合していませんか?
  3. サーバー側のログを確認する: Flaskアプリケーションのログ(特にデバッグレベル)を確認し、リクエストがサーバーに到達しているか、エラーが発生していないかを確認します。Flask-CORS自体もログを出力することがあります (ロガー名は ‘Flask-Cors’)。
  4. 簡単なケースで試す: 問題が複雑な場合、一時的に最も緩い設定 (CORS(app)) にしてみて、それで動作するか確認します。もし動作するなら、設定を徐々に厳しくしていき、どの設定が問題を引き起こしているかを特定します。

CORSエラーは、多くの場合、サーバー側の設定ミスが原因です。落ち着いてヘッダーと設定を確認すれば、解決できるはずです。🕵️‍♀️

まとめ ✨

この記事では、PythonのFlaskフレームワークでクロスオリジンリソース共有 (CORS) を扱うための強力な拡張機能、Flask-CORSについて詳しく解説しました。

同一オリジンポリシーの基本的な概念から始まり、なぜCORSが必要なのか、Flask-CORSのインストール方法、そして基本的な使い方を見てきました。さらに、origins, methods, allow_headers, expose_headers, supports_credentials, max_age といった詳細な設定オプションを、具体的なコード例とともに紹介し、アプリケーション全体、リソースごと、またはルートごとにCORSポリシーを柔軟に設定する方法を学びました。

SPAとの連携やマイクロサービスアーキテクチャなど、Flask-CORSが役立つ典型的なユースケースを確認し、origins="*" の安易な使用や supports_credentials=True の設定に伴うリスクなど、セキュリティ上の重要な考慮事項についても触れました。最後に、プリフライトリクエストの仕組みや、CORSエラーが発生した場合のデバッグ方法についても解説しました。

Flask-CORSは、Flaskアプリケーションでクロスオリジン通信を安全かつ効率的に実現するための不可欠なツールです。提供されているオプションを理解し、セキュリティのベストプラクティスに従って適切に設定することで、モダンなWebアプリケーション開発における一般的な課題の一つであるCORSの問題をスムーズに解決することができます。ぜひ、あなたのFlaskプロジェクトでFlask-CORSを活用してみてください! 🎉

コメント

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