CORSを絶対に理解する
対象者
「CORSがなんなのかわからない」
「とりあえず調べた記事でテキトーに解決したけど、根本的に何がダメだったのかがわからない」
みたいな方を対象に、CORSについて細かく解説しています。
記事を読み終えれば、CORSについて理解でき、どんな記述が必要なのかがわかるようになると思います。
また、フロントエンドしか触らない方も読んでいただけると、フロント側のエラーかと思っていたけれど実はバックエンドの方の設定が漏れていた...みたいなケースにも対応できるようになると思います。
今回サンプルコードを置いていますが、フロントはJavaScript、バックエンドはGolang(Echo)で記述しています。
まずはCORSとは何か
CORSはオリジン間リソース共有という意味です。
追加のHTTPヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンに選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組み
そもそもオリジンとは?
オリジンとは、ウェブコンテンツにアクセスするために使われるURLの スキーム(プロトコル)、ホスト(ドメイン)、ポート によって定義される。
http://example.com:8080/sample1/index.html
をスキーム、ホスト、ポートに分けると以下のようになります。
スキーム: http
ホスト: example.com
ポート: :8080
例)同一オリジン
http://example.com:8080/sample1/index.html
http://example.com:8080/sample2/index.html
上記はパスが異なるだけで、スキーム、ホスト、ポートは同じであるので同一オリジンであると言えます。
例)異なるオリジン
スキームが違うため、同一オリジンではありません。
http://example.com/sample1/index.html
https://example.com/sample2/index.html
ホストが違うため、同一オリジンではありません。
http://example.com/sample1/index.html
http://www.example.com/sample1/index.html
同一オリジンポリシー
次になぜCORSがあるのかを理解するために同一オリジンポリシー
について説明します。
同一オリジンポリシーは重要なセキュリティーの仕組みであり、あるオリジンによって読み込まれた文書やスクリプトが、他のオリジンにあるリソースにアクセスできる方法を制限するもの。
同一オリジンポリシーがあることによって、悪意のあるドキュメントを隔離し、起こりうる攻撃を減らすことを目的としています。
この仕組みによって、Fech APIなどでリソースをリクエストする場合には、同じオリジンへのリソースへのリクエストしか行うことができなくなります。
同一オリジンポリシーがない場合(Bad Case)
悪意のあるサイトのオリジンを http://akui.com
とし、
欲しい情報があるサイトのオリジンを https://jouhou.com
とします。
- 送られてきたメールの中に悪意のある人が作成したWebサイトへのリンクがあった
- ユーザはリンク先のWebサイトへ遷移
- 遷移先のブラウザ上で悪意のあるJSファイルがダウンロード・実行される
- JSが実行されると攻撃対象(攻撃者が欲しい情報があるサイト)へのGETリクエストが送られる
- 情報を取得したのち、攻撃対象の情報が悪意のあるWebサイトへ返される
- 悪意のある人へ個人情報などが漏洩する
同一オリジンポリシーがある場合(Good)
CORSがなかった状態のブラウザ(過去のブラウザ)の流れ
悪意のあるサイトのオリジンを http://akui.com
とし、
欲しい情報があるサイトのオリジンを https://jouhou.com
とします。
- 送られてきたメールの中に悪意のある人が作成したWebサイトへのリンクがあった
- 遷移先のブラウザ上で悪意のあるJSファイルがダウンロード・実行される
- JSが実行されると攻撃対象(攻撃者が欲しい情報があるサイト)へのGETリクエストが送られる
- 同一オリジンポリシーにより、リクエストがブロックされる
このように、同一オリジンポリシーがあることによって情報漏洩は避けることはできるようになりました。
しかし、AJAXの普及・発展により、異なるオリジン(主に異なるホスト)のAPIを呼び出したいという動機が生まれました。
この問題を解決するために、CORS(オリジン間リソース共有)が考案されました。
CORSの基本特性について
- 提供する機能
- クロスオリジン(異なるオリジン)のリソースアクセスを提供
- オリジン単位のアクセス制御を提供
- 後方互換性
- 従来動作しているサイトがCORSの元でも動作する
- 従来の(CORS)を使わないサイトの性能劣化はほとんどない
- 従来セキュリティ上問題のないサイトがCORSの元でも安全に動作する
- HTTPヘッダを用いたアクセス制御
-
リクエストヘッダ:
Origin
-
レスポンスヘッダ:
Access-Controll-Allow-Origin
-
単純リクエスト
については無条件でリクエストを送信して、レスポンスヘッダ
によりJSがリソースを受け取れるか否かを判断する -
単純ではないリクエスト
についてはプリフライトリクエスト
によりリクエスト送信の許可を得る
基本特性については上記の通りです。
ウェブアプリケーションは、自分とは異なるオリジンにあるリソースをリクエストするとき、
オリジン間HTTPリクエスト
を実行します。
オリジン間リクエストとは、例えばhttps://example.com/
で提供されているウェブアプリケーションのJSコードが、fetch
などを利用してhttps://example-resouce.com/data.json
へリクエストを行うことを指します。
CORS対応ブラウザの挙動
CORSは追加のHTTPヘッダーを使用
してクライアントとサーバのやりとりを行います。
つまり、リクエストヘッダには
Origin
Access-Controll-Request-http://example.com
の2つのHTTPヘッダを追加する必要があります。
またレスポンスヘッダには
Access-Controll-Allow-http://example.com
のようなHTTPヘッダを追加してあげる必要があるということです。
API側でCORS許可がない場合
APIを使用する側のサイトオリジンをhttp://example.com
とし、
APIを提供するサーバを持つサイトオリジンをhttp://api.example.com
とします。
- クライアントは、APIを使用するサイトへのリクエストを送り、リソースを取得するためのJSファイルをダウンロードする
- JSファイルはリソースを取得するため、APIを提供するサーバへのオリジンに対しHTTPリクエストを行う
- HTTPリクエストには、追加のHTTPヘッダとして
Origin: http://example.com
を付与する - リクエストを受けたAPIサーバは、JSONなどのリソースをHTTPレスポンスとしてリクエスト元へ返却する
- しかしHTTPレスポンスには、
Access-Controll-Allow-http://example.com
が付与されていない - この場合、CORSは許可されていないこととなり、リクエスト元はリソースを取得することができない
API側でCORS許可がある場合
APIを使用する側のサイトオリジンをhttp://example.com
とし、
APIを提供するサーバを持つサイトオリジンをhttp://api.example.com
とします。
- クライアントは、APIを使用するサイトへのリクエストを送り、リソースを取得するためのJSファイルをダウンロードする
- JSファイルはリソースを取得するため、APIを提供するサーバへのオリジンに対しHTTPリクエストを行う
- HTTPリクエストには、追加のHTTPヘッダとして
Origin: http://example.com
を付与する - リクエストを受けたAPIサーバは、JSONなどのリソースをHTTPレスポンスとしてリクエスト元へ返却する
- その際、HTTPレスポンスに追加のHTTPヘッダとして
Access-Controll-Allow-http://example.com
を付与する - リクエスト元のオリジン
http://example.com
のCORSが許可されたため、リクエスト元はリソースを受け取ることができる
このようにして、同一オリジン間ポリシーを守りながら、CORSは異なるオリジン間でのリソースの共有を実現しているわけです。
ここまでの説明を実際のブラウザで確認してみる
ローカル環境で確認できるようにコードを書いてみました。
フロント側はlocalhost:8080
バックエンド側はlocalhost:8081
でサーバーを起動させます。
そして、フロントからはlocalhost:8081/users
へリソースを取得するためにfetchを使います。
今回の場合にはポート番号が異なっていることから、異なるオリジンであると言える。そのため、同一オリジンポリシー
が働くため、CORSの設定
が必要です。
今回はフロントはJavaScript、バックエンドはGolang(Echo)でサンプル作成しています。
フロント
fetchでlocalhost:8081/users
に対してリクエストを送っています。
そして取得したリソースを使って、DOM操作をしています。
button = document.getElementById("getUser")
users = document.getElementById("users")
async function getUsers() {
const res = await fetch("http://localhost:8081/users", { mode: "cors" })
const data = await res.json()
data.forEach(u => {
item = document.createElement("li")
item.innerText = `${u.name}は${u.age}歳です。`
users.appendChild(item)
})
}
button.addEventListener("click", getUsers)
バックエンド
GolangのフレームワークであるEchoを使ってmiddleware内でCORSの設定をしていきます。
arrowOrigins
で許可するオリジンを配列で指定します。今回の場合はリクエスト元のオリジンがhttp://localhost:8080
のみなので以下のように記述しています。
また、middleware.CORSWithConfig
でCORSの詳細な設定をできるのですが、ここで先ほど設定したオリジンを割り当てています。
つまり、http://localhost:8080
からのオリジン間リクエストを許可するというコードになっています。
func main() {
e := echo.New()
e.HideBanner = true
arrowOrigins := []string{"http://localhost:8080"}
e.Use(middleware.Logger())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: arrowOrigins,
}))
e.GET("/users", GetUsers)
e.Logger.Fatal(e.Start(":8081"))
}
動作確認
-
フロント側にユーザ取得ボタンを配置
-
users取得時のリクエストヘッダを確認
Origin: http://localhost:8080
が確認できる。
-
users取得時のレスポンスヘッダを確認
Access-Controll-Allow-Origin:http://localhost:8080
が確認できる。
-
取得したデータを埋め込むことができた
プリフライトリクエストとは(単純ではないリクエスト)
これまで単純なリクエストの場合のCORSをみてきました。
次に 単純ではないリクエスト の場合はどうなるかを説明していきます。
プリフライトリクエストは以下のように定義されています。
「プリフライト」リクエストは始めに OPTIONS メソッドによる HTTP リクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうかを確かめます。サイト間リクエストがユーザーデータに影響を与える可能性があるような場合に、このようにプリフライトを行います。
プリフライトリクエストを理解する前に、単純リクエストについて知る必要があります。
単純リクエストとは
- 許可されているメソッドのうちいずれかであること
GET
HEAD
POST
- 手動で設定できるリクエストヘッダは以下のいずれかであること(ユーザエージェントによって自動的に付与されるヘッダーを除く)
Accept
Accept-Langage
Content-Langage
Content-Type
- Content-Typeヘッダは以下のいずれかであること
application/x-www-form-urlencoded
multipart/form-data
text/plain
これらの条件を全て満たすものを 単純リクエスト と定義されています。
プリフライトリクエスト送信の流れ
単純リクエストでない場合のプリフライトリクエストが送られる流れを確認します。
-
PUTリクエスト
でHTTPリクエストを送信 - 単純リクエストではないため、プリフライトリクエストが送られる
- プリフライトリクエストは、
OPTIONS api/ HTTP1.1
Access-Control-Request-Method: PUT
-
Origin: http://example.jp
といったHTTPヘッダを追加してHTTPリクエストを飛ばす。このとき、実際の内容はまだ送られていない。
- プリフライトリクエストが送られてきたサーバは、
Access-Control-Allow-Method: PUT
-
Access-Control-Allow-Origin: http://example.com
のように許可するHTTPメソッド・オリジンの情報を返却する。
- プリフライトリクエストで許可が得られたため、PUTリクエストが送信される
- あとは単純リクエストの手順と同じ
のような流れでプリフライトリクエストが送られます。
プリフライトリクエストを確認するためのコード
ローカルでプリフライトリクエストを確認できるようにコードを書いてみました。
今回はフロント側をhttp://localhost:1234
、バックエンドをhttp://localhost:8888
で起動します。
フロント
単純リクエストのコードに追加して、以下のような削除する用の関数を追加しています。
methodにはDELETE
を設定しているため、削除実行時には単純リクエストではないHTTPリクエストを送信することになります。
fetch(`http://localhost:8888/albums/delete/${id.value}`,{mode: cors, method: "DELETE"})
getButton = document.getElementById("getAlbum")
deleteButton = document.getElementById("deleteAlbum")
albums = document.getElementById("albums")
async function getAlbum() {
const res = await fetch("http://localhost:8888/")
const data = await res.json()
while (albums.firstChild) {
albums.removeChild(albums.firstChild)
}
data.forEach(d => {
item = document.createElement("li")
item.innerText = `ID: ${d.ID} アルバム名:${d.title} 作者:${d.artist} 価格:${d.price}`
albums.appendChild(item)
})
}
async function deleteAlbum() {
id = document.getElementById("id")
await fetch(`http://localhost:8888/albums/delete/${id.value}`,{
mode: "cors",
method: "DELETE"
})
id.value = ""
}
getButton.addEventListener("click", getAlbum)
deleteButton.addEventListener("click", deleteAlbum)
バックエンド
バックエンド側では許可するオリジンに加えて新しく
AllowMethods: []string{"GET", "DELETE", "OPTIONS"}
を追加しています。
これでこのサーバはGET
DELETE
OPTIONS
メソッドを許可することを明示しています。
func main() {
e := echo.New()
e.HideBanner = true
arrowOrigins := []string{"http://localhost:1234"}
e.Use(middleware.Logger())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: arrowOrigins,
AllowMethods: []string{"GET", "DELETE", "OPTIONS"},
}))
e.GET("/", GetAlbums)
e.DELETE("/albums/delete/:id", Delete)
e.Logger.Fatal(e.Start(":8888"))
}
まずはプリフライトリクエストが飛ばない、単純リクエストを確認します。
その次に単純リクエストではないリクエストを送信することで、プリフライトリクエストがどのように送信されているのか、request headerやresponse headerの内容を確認してみます。
動作確認
- 取得ボタンを押すとDBに保存されているデータが取得できる
- GETメソッドで単純リクエストに該当するため、プリフライトリクエストは行われていない。
リクエストの結果
request header
response header
- 次にIDを指定して削除ボタンを押す。
- 今回はDELETEメソッドのため、プリフライトリクエストが送信されていることが確認できる。
またその際のHTTPメソッドは、OPTIONSメソッドとして送信されている。
preflight request
options method
- プリフライトリクエストでお伺いを立てたあとに、実際のリクエスト内容を送信し、レスポンスを受け取る。
request header
response header
資格情報を含むリクエストについて
資格情報とは以下を指します。
Cookie
認証ヘッダー
TLS クライアント証明書
今回はCookie付きのリクエストを送る場合、どのようなリクエスト・レスポンスの設定が必要なのかを説明します。
リクエストでfetchを使う場合、credentials
オプションを設定することができます。
これはfetchがリクエストと一緒にCookie(資格情報)を送るべきかを指定するものです。
-
same-origin
: デフォルト。クロスオリジンリクエストの場合はCookieを送信しない。同一オリジンの場合のみCookieなどの資格情報を送信することができる。 -
include
: 資格情報を含むリクエストを、クロスオリジンの場合でも送信することができる。include を設定した場合、レスポンスを返すサーバからAccess-Control-Allow-Credentials
を要求する。 -
omit
: 同一オリジンのリクエストの場合でも資格情報を送信しない。
const res = await fetch("http://localhost:8081/users", {
credentials: "include",
})
const data = await res.json()
このような単純リクエストの場合プリフライトリクエストは行われないですが、上記にも書いたようにレスポンスを含むサーバからAccess-Control-Allow-Credentials
を要求するため、これが含まれていないレスポンスを拒否してしまいます。
そのため、サーバ側ではAccess-Control-Allow-Credentials
をtrue
として返すように設定しなければいけません。
func main() {
e := echo.New()
e.HideBanner = true
arrowOrigins := []string{"http://localhost:8080"}
e.Use(middleware.Logger())
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
AllowOrigins: arrowOrigins,
AllowCredentials: true, // ここ
}))
e.GET("/users", controller.GetUsers)
e.Logger.Fatal(e.Start(":8081"))
}
またサーバ側は、資格情報付きのリクエストに返答する場合には、以下の条件を満たさなければならないと決まっています。
-
Access-Control-Allow-Origin
で"*"
(ワイルドカード)を指定してはいけない。明示的にオリジンを指定する必要がある。 -
Access-Control-Allow-Headers
で"*"
(ワイルドカード)を指定してはいけない。明示的にヘッダー名を指定する必要がある。 -
Access-Control-Allow-Methods
で"*"
(ワイルドカード)を指定してはいけない。明示的にメソッド名を指定する必要がある。
まとめ
かなり長くなってしまいましたが、CORSについてまとめてみました。
オリジン、同一オリジンポリシー、単純リクエスト、各ヘッダーの設定などなどたくさん知ることはありましたが、一度理解してしまえば設定自体は簡単だったように思います。
また、MDNではCORSのエラーというまとめのページがあるため、CORS関係で詰まったときには訪れると良いのではないかと思います。
誰かのCORSの理解の役に立てれば幸いです。
参考
Discussion
Nice Information, For frontend concepts, check:- https://frontendinterviewquestions.com