CORSについて勉強する
はじめに
CORS とは Cross Origin Resource Sharing の略で、クロスオリジンでのリソース共有を可能にする仕組みです。僕はこれまでふんわりとしか CORS のことを認識していなくて、いざ CORS を有効にしてくださいって言われてもどうすればいいのかわかりませんでした。今回は実際にコードを書きながら、CORS の仕組みを勉強したので、記録にとこの記事を書きました。
また、この記事に使ったコードは以下のリポジトリにあります。リポジトリにはこの記事書いてないようなパターンもいくつか試している例がありますので、よければそちらも参照してみてください。
同一生成元ポリシー (Same-Origin Policy)
生成元(Origin)については知ってる人も多いかと思います。オリジンとはプロトコル、ホスト、ポート番号の組み合わせですね。
https://sample.com:8080 ----- ----------- ---- | | | +- protocol +-- host +-- port
そして、同一生成元ポリシーとは、異なるオリジンのリソースへのアクセスを制限する仕組みです。これによって、あるオリジンに存在する悪意のあるサイトの JavaScript コードが別のオリジンに存在するサイトのリソースを盗むといったことを防ぐことができます。同一生成元ポリシーによって制限される操作の例として、XMLHttpRequest や Fetch API を使った異なるオリジンとの非同期通信が挙げられます。
CORS
同一生成元ポリシーによって、XMLHttpRequest や Fetch API を使った異なるオリジンとの非同期通信が制限されると書きましたが、以下の例について考えてみましょう。
ありうる構成ではあると思いますが、同一生成元ポリシーによって、APIサーバとの通信が制限されてしまいます。この状況を打開できるのが CORS です。
単純な例
以下のような単純な例を考えてみましょう。
APIサーバ
from fastapi import FastAPI app = FastAPI() @app.get("/me") def me(): return { "name": "john", "about": "I want to be a shellfish.", "homepage": "https://shellfi.sh" }
Webサーバ
from fastapi import FastAPI from fastapi.responses import HTMLResponse app = FastAPI() @app.get("/", response_class=HTMLResponse) def index(): with open("./index.html") as f: return f.read()
index.html
<!doctype html> <html lang="en" class="h-100"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>Me</title> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous"> </head> <body class="h-100 bg-dark"> <div class="container h-100"> <div class="row align-items-center h-100"> <div class="mx-auto"> <div class="card h-100 justify-content-center" style="width: 18rem;"> <div class="card-body"> <h5 class="card-title" id="name"></h5> <p class="card-text" id="about"></p> <a href="#" class="btn btn-info" id="homepage">Homepage</a> </div> </div> </div> </div> </div> <script> window.onload = () => { fetch("http://127.0.0.1:8000/me") .then(response => response.json()) .then(me => { let name = document.getElementById("name"); let about = document.getElementById("about"); let homepage = document.getElementById("homepage"); name.innerText = me.name; about.innerText = me.about; homepage.href = me.homepage; }) .catch(error => { console.log(error); let name = document.getElementById("name"); let about = document.getElementById("about"); let homepage = document.getElementById("homepage"); name.innerText = "unknown"; about.innerText = "unknown"; homepage.href = "unknown"; }); } </script> </body> </html>
上記のファイルが同一ディレクトリに存在する状態で以下のコマンドを実行します。
$ uvicorn api:app --port=8000 $ uvicorn web:app --port=8001
この例では、Web サーバのオリジン(http://127.0.0.1:8001
)に存在するサイトから API サーバのオリジン(http://127.0.0.1:8000
) に対して、Fetch API を使って、クロスオリジンなリソース要求をしています。データを取得できた場合は、プロフィールが表示され、取得できなかった場合はプロフィールに unknown が表示されます。
ブラウザで確認してみると
開発者コンソールを確認すると
通信がブロックされていることがわかります。また、エラーメッセージで親切に Access-Control-Allow-Origin
ヘッダがありませんと書いていますね。これに従って、API サーバ側にこのヘッダをつけてみましょう。
from typing import Optional from fastapi import FastAPI, Response, Header ... def me(response: Response, origin: Optional[str] = Header(None)): response.headers["Access-Control-Allow-Origin"] = "http://127.0.0.1:8001" ...
今度はどうでしょう。
表示されましたね。このように、Access-Control-Allow-Origin
ヘッダを使って、自身のリソースをどのオリジンに対してアクセス可能にするか指定することができます。また、今回は Access-Control-Allow-Origin: http://127.0.0.1:8001
と指定しましたが、Access-Control-Allow-Origin: *
としても動くはずです。*
はすべてのオリジンからそのリソースにアクセスできることを意味しますので。
プリフライトリクエストが必要な例
今度は、次の場合を考えてみましょう。PUT
メソッドでリソースの一部(今回はプロフィール内の名前)を更新します。
API サーバ
... name_list = ["john", "joshua"] profile = { "name": "john", "about": "I want to be a shellfish.", "homepage": "https://shellfi.sh" } ... @app.put("/me") def put(response: Response): response.headers["Access-Control-Allow-Origin"] = "http://127.0.0.1:8001" global name_list name_list = name_list[1:] + name_list[:1] profile["name"] = name_list[0] return profile
index.html
<a href="#" class="btn btn-info" id="homepage">Homepage</a> <button class="btn btn-warning" onclick="update()">Update</button> ... <script> window.onload = () => { ... } function update() { fetch("http://127.0.0.1:8000/me", { method: "PUT", }).then(response => { let name = document.getElementById("name"); let about = document.getElementById("about"); let homepage = document.getElementById("homepage"); let me = response.json(); name.innerText = me.name; about.innerText = me.about; homepage.href = me.homepage; }).catch(error => { console.log(error); }); } </script>
API サーバでも、Access-Control-Allow-Origin
ヘッダをセットしているので大丈夫そうに見えますが、Update ボタンを押しても以下のようにうまくいきません。
Access-Control-Allow-Origin
ヘッダをセットしたのに、Access-Control-Allow-Origin
ヘッダがないと言われています。なぜこんなことが起きたのでしょう。Networkパネルにヒントがあります。
このように、我々が預かり知らぬところで OPTIONS
メソッドのリクエストを密かにサーバに飛ばしています。実は、PUT
メソッドのような 単純リクエストではないリクエスト (単純リクエストとは何かというのは MDN を確認してみてください)を送信する場合、事前にサーバに「これからこんな感じのリクエストを送りますが大丈夫ですか」ということを確認するためのリクエストを送信します。これが プリフライトリクエスト です。
そもそも、なぜプリフライトリクエストが必要なのでしょうか。MDN には次のような記述があります。
サーバーの情報に副作用を引き起こすことがある HTTP のリクエストメソッド (特に GET 以外の HTTP メソッドや、特定の MIME タイプを伴う POST) のために、ブラウザーが HTTP の OPTIONS リクエストメソッドを用いて、あらかじめリクエストの「プリフライト」 (サーバーから対応するメソッドの一覧を収集すること) を行い
つまり、そもそもリクエストが送信されること自体に問題があるような場合 (CSRF とか) には、予めプリフライトリクエストが送信されるのです。
プリフライトリクエストへのレスポンスでは、対象リソースに対して許可されているメソッド、ヘッダの種類などを含めます。
... @app.options("/me", status_code=204) def options(response: Response): response.headers["Access-Control-Allow-Origin"] = "http://127.0.0.1:8001" response.headers["Access-Control-Allow-Methods"] = "GET,PUT,OPTIONS"
OPTIONS
メソッドのハンドラを定義してから、ブラウザで Update ボタンを押して見ると次のように名前が更新されていることが確認できると思います。
その他
CORS に関連するヘッダーは Access-Control-Allow-Origin
と Access-Control-Allow-Methods
以外にもあります。たとえば、Access-Control-Allow-Headers
でリクエストに使用できるヘッダを指定できたり、Access-Control-Allow-Credentials
でフロントエンドの JavaScript コードに Cookie などの資格情報を公開するか制御できたりします。これらのコード例に関しても、記事冒頭の GitHub リポジトリに含めています。
おわりに
CORS をちゃんと勉強したことがなかったので、ここらへんのこと何もわかっていませんでした… 今後は、Content Security Policy について勉強しようかなと思います。
参考文献
www.slideshare.net