Flask-Cors を使って Flask を Cross Origin Request に対応する
Flask で作った REST API を Cross Origin Request に対応する必要があり、Flask-Cors を使用しました。その際に調べた Flask-Cors の使い方のメモです。
やりたいこと
Flask で作った REST API と Web フロントコンテンツが Origin が異なっていたので、CORS 対応が必要でした。 通常 Production として使用するような環境では Flask の前に Nginx などの Web Server が立っており、 そちらで CORS 対応をすると思いますが、今回は簡易的に Flask のみで稼働させていたので、Flask で CORS 対応を行いました。
調べてみると Flask-Cors という Flask の Extension パッケージがありましたので、こちらを使用しました。
環境
- OS
- Microsoft Windows 20H2
- ブラウザ
- Microsoft Edge 109.0.1518.78
- Python
- Python 3.9.6
- Flask
- Flask 2.2.2
- Flask-Cors
- Flask-Cors 3.0.10
Cross-Origin Resource Sharing (CORS)
CORS については オリジン間リソース共有 (CORS) - HTTP | MDN で詳細に説明されています。ここでは簡単な例示に留めさせてもらいます。途中 Flask-Cors も使っていますが、Flask-Cors の使い方は次項で説明します。また HTTP ヘッダーが何度も出てきますが、長くなりますので、適宜本説明に関係のないヘッダーは省いて掲載しています。
準備
まず、Flask を http://127.0.0.1:5000
(= Origin) で Listen させます。そして Web コンテンツ (ここでのメインは JavaScript) を http://127.0.0.1:5500
(= Origin) にホストします(Web コンテンツのホストは VSCode 拡張機能の Live Server を使用)。Origin は以下の通りポート番号も含まれますので同じホスト 127.0.0.1
でもこれらは Cross-Origin となります。
ウェブコンテンツのオリジン (Origin) は、ウェブコンテンツにアクセスするために使われる URL の スキーム (プロトコル)、 ホスト (ドメイン)、 ポート番号 によって定義されます。 - Origin (オリジン) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
CORS Request
Flask で以下のように簡単な API を作ります。
次に以下の JavaScript で Request してみます。
すると Edge の開発者ツールのコンソールに以下のようなエラーメッセージが表示されます。
Access-Control-Allow-Origin
ヘッダーにリクエスト元 (http://127.0.0.1:5500
) がないのでブロックされたと言われています。
Edge の開発者ツールのネットワークでレスポンスヘッダーを見てみると以下の通りで、Access-Control-Allow-Origin
ヘッダーはないです。
さらにリクエストヘッダーを見てみると以下の通りとなっており、Cross-Origin Request 以外では見かけない Origin
ヘッダーが付与されています。
この Origin
が、レスポンスヘッダーの Access-Control-Allow-Origin
に含まれている必要があります。
またこのリクエストのことを CORS Request と呼びます。(以前は単純リクエストと呼ばれていたもの) WHATWG の Fetch Standard で仕様策定されています。
CORS Requestの対応
それでは Python 側のコードを以下の通り変更します。ポイントは @cross_origin
デコレーターです。
フロント側は手を入れずにもう一度リクエストしてみます。
すると Edge の開発者ツールのコンソールを見ると Response が返って来ていることが確認できると思います。
さらに Edge の開発者ツールのネットワークでレスポンスヘッダーを確認すると、以下の通り Access-Control-Allow-Origin
ヘッダーに http://127.0.0.1:5500
が入って返ってきていることが確認できます。これにより Cross-Origin Request が許可された形となっています。
次に POST メソッドを試してみます。Python 側コードは POST
を追加します。長くなるのでここではデコレーター部分だけ記載します。
フロント側は以下の通り変更します。
こちらも特に問題なく Response が返って来ることが確認できます。
CORS-preflight request
ここからさらにフロント側に手を入れて Content-Type
ヘッダーを追加します。
すると Edge の開発者ツールのコンソールに以下のようなエラーメッセージが表示されます。
次は Content-Type
ヘッダーは許可されていないのでブロックされたと言われています。さらにプリフライトなるワードが登場しています。
さてプリフライトとは何なのでしょう。少し長くなりますが MDN の説明を引用します。
CORS のプリフライトリクエストは CORS のリクエストの一つであり、サーバーが CORS プロトコルを理解していて準備がされていることを、特定のメソッドとヘッダーを使用してチェックします。
これは OPTIONS リクエストであり、 Access-Control-Request-Method,Access-Control-Request-Headers, Origin の 3 つの HTTP リクエストヘッダー使用します。
プリフライトリクエストはブラウザーが自動的に発行するものであり、通常は、フロントエンドの開発者が自分でそのようなリクエストを作成する必要はありません。これはリクエストが"to be preflighted"と修飾されている場合に現れ、単純リクエストの場合は省略されます。
- Preflight request (プリフライトリクエスト) - MDN Web Docs 用語集: ウェブ関連用語の定義 | MDN
つまりブラウザが勝手にメインのリクエスト (今回は POST リクエスト) の前に OPTIONS
リクエストでサーバー側に確認し、メインのリクエストを行ってもよいか確認しているものになります。これを CORS-preflight request と呼びます。
Edge の開発者ツールのネットワークを見ると cors
エンドポイントに 2 つのリクエストがされていることが確認できます。そのうちの 1 つは OPTIONS
リクエストになっています。そのリクエストヘッダーは以下の通りで、Access-Control-Request-Headers
、Access-Control-Request-Method
、Origin
が含まれています。
これに対し、レスポンスヘッダーは以下の通りで、メソッドやヘッダーに対する応答はありません。
CORS-preflight request の対応
それでは Python 側で以下の通り修正をします。
フロント側は手を入れずに、再度リクエストします。
すると Edge 開発者ツールのコンソールに Response が返ってきていることが確認できます。
さらに Edge 開発者ツールのネットワークでプリフライトリクエストのレスポンスヘッダーを確認すると、以下の通り Access-Control-Allow-Headers
や Access-Control-Allow-Methods
が返って来ていることが確認できます。これによってメインのリクエストを送ることができ、レスポンスが得られたということです。
さてそもそもなぜ Content-Type: application/json
を追加するとプリフライトリクエストが発生したのでしょうか。
プリフライトリクエストが発生する条件は様々で、詳細は オリジン間リソース共有 (CORS) - HTTP | MDN で説明されていますが、今回は Content-Type
ヘッダーに application/x-www-form-urlencoded
、multipart/form-data
、text/plain
以外を設定したためです。
プリフライトリクエストが発生するとブラウザはサーバー側から Access-Control-Allow-Origin
に加え Access-Control-Allow-Headers
、Access-Control-Allow-Methods
を受け取ってアクセス許可があるか確認します。Flask-Cors の cross_origin
デコレーターでは methods
パラメーターで Access-Control-Allow-Methods
で返すメソッド、allow_headers
パラメーターで Access-Control-Allow-Headers
で返すヘッダーを設定できます。なおこれらのパラメーターのデフォルト値は methods
は [GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE]
、allow_headers
は *
となっているので、実は上の例では敢えて methods=["GET"]
と書かなければ上手くいきます。説明の都合設定したのと、実際はフルオープンで運用することはないと思われるためです。
CORS 対応まとめ
かなり長くなってしまいましたが、CORS の対応としては、
プリフライトが発生しないケース (CORS request) ではサーバー側が Access-Control-Allow-Origin
ヘッダーにアクセス許可するオリジンを入れて返す必要がある、
プリフライトが発生するケース (CORS-preflight request) では、上記に加え、Access-Control-Allow-Headers
ヘッダーに許可する HTTP ヘッダー、Access-Control-Allow-Methods
ヘッダーに許可する HTTP メソッドを入れて返す必要がある、
となります。
Flask-Cors の使い方
インストール
pip
でインストールします。
アプリケーション全体での CORS ポリシー設定
上記の例では cross_origin
デコレーターでの設定を紹介しましたが、アプリケーション全体で CORS ポリシーを設定することもできます。
以下の例では /
にも /api/api1
にも origins=["http://127.0.0.1:5500"], methods=["GET", "POST"]
のポリシーが適用されます。
リソース毎の CORS ポリシー設定
CORS
に resources
パラメーターを渡すことでリソース毎に CORS ポリシーを設定できます。
以下の例では /api/*
のリソースにのみ CORS ポリシーを設定しています。/
には Cross-Origin Request できません。
cross_origin デコレーターを使用したリソース毎の CORS ポリシー設定
上の例でも使用した cross_origin デコレーターを使用した CORS ポリシー設定です。
その他
ここでは origins
や methods
といった代表的なパラメーターを紹介しましたが、それ以外のパラメーターは本家 API Docs – Flask-Cors 3.0.10 documentation を参照してみてください。
あとがき
簡単にとか言いながら CORS の説明でかなり字数を割いてしまい Flask-Cors の説明なのか CORS の説明なのかわからない記事になってしまいました。Flask-Cors は本家ドキュメント以外に細かい使い方が説明されているページが見当たらなかったので纏めてみました。