やんわり分かる Origin や CORS -とりあえず手を動かしてみよう-
Intro
自分のための概要纏めも含めて Sane-Site 等について簡単に触りつつまとめていこうかなと思います。「まあ、動かしてみたら何かしら分かるやろ(^^)」という緩い意志の基、前半である程度お話をして、後半では実際に簡単なサイトで動作を確認した感じをまとめてます。
なお、以下の英語のブログがすごい活かしてるので、これを理解出来れば正直この記事は読まなくても大丈夫な気もしますが実際に動作確認できるし折角なので読んで。
あと、もし Azure アカウント持ってる方がいらっしゃったら、Free で試せるような内容なので気が向いたら試してみても良いのではと思います。
基本知識編
オリジン
スキーム、ドメイン(ホスト名)、ポート番号から構成されるもの。
ブラウザの URL に入力するようなものをイメージしたらよいかと。
例えば、https://www.hogehoge.com を例にすると以下のようになります。
- スキーム : https
- ドメイン(ホスト名) : www.hogehoge.com
- ポート番号 : 443(https の標準で今回は省略されていた形式)
若干のポイントとして、エンドポイントまでは定義されていなさそうですね。(https://www.hogehoge.com/endpointB みたいな感じ)
Cross Origin Resource Sharing (CORS)
Web系や Web API 等を触ってて、こういうエラーに遭遇したことある方は多いんじゃないかなと思います。(私比) どういう時に起きているかというと上記のオリジンを跨いでリソースを共有しようとした時に発生してます。
例えば、Client からサーバ A にアクセスした際に、サーバA では表示する画像を取得するためにサーバ B へとリクエストを送信しているとします。この時、サーバ A とサーバ B では上述のオリジンを跨いで別のサイトからリソースを取得しようとしています。
その時に、サーバ B からしたら、よく分からないサーバから自分のリソースを取得されそうになっているため「このリクエストは本当に大丈夫なのか???」という気分になります。そしてブロックされた結果エラーになります。
そう、こんなイメージです。
サーバ A 的にはサーバ B を友達だと思って「友達(Client)に情報あげたいんだけど、教えてくれない?」って聞いたら、サーバ B 的には「え?誰...?信用できる人なのか...?」って感じでサーResponse を返さないようなイメージです。(イメージです。)
具体的な例を以下にいくつか纏めました。
Origin A | Origin B | same/cross-origin |
---|---|---|
https://www.hogehoge.com | https://www.hogehoge.com:443 | same-origin |
https://www.hogehoge.com | https://www.hogehoge.com/endB | same-origin |
https://www.hogehoge.com | http://www.hogehoge.com | cross-origin |
https://www.hogehoge.com | https://login.hogehoge.com | cross-origin |
https://www.hogehoge.com | https://www.hogehoge.com:800 | cross-origin |
https://www.hogehoge.com | https://www.fugafuga.com | cross-origin |
この CORS は API サーバを守るための仕組みです。上記のシステムを動作させるようにするにはサーバ B にてサーバ A からリクエストを受け付けるようにしないとダメっぽいので、その辺は後半で試してみます。
ブラウザの SameSite 属性
上記の origin と似た概念で若干違うのが Site の概念です。
サイトは、イメージとしては hogehoge.com のように、トップ レベルドメイン(TLD)とその一つ上の内容で決定します。(正確には effective TLD ?)
そして、先ほどの CORS は API サーバの観点でしたが、SameSite はブラウザ側の動作にも影響します。というのも、最近はブラウザにて header の cookie を確認して、SameSite 属性がどのような値であるかを確認するようになっていて、値によっては cookie を引き継げない等が起こっています。これはセキュリティ的な面も加味してのことみたいですね。
ちなみに、この値がデフォルトでは Lax という値になり、そこに引っかかって作ったシステムが動作しないということもあるみたいですね。
具体的な例を以下にいくつか纏めました。
Origin A | Origin B | same/cross-site |
---|---|---|
https://www.hogehoge.com | https://www.hogehoge.com:443 | same-site |
https://www.hogehoge.com | https://www.hogehoge.com/endB | same-site |
https://www.hogehoge.com | http://www.hogehoge.com | same-site |
https://www.hogehoge.com | https://login.hogehoge.com | same-site |
https://www.hogehoge.com | https://www.hogehoge.com:800 | same-site |
https://www.hogehoge.com | https://www.fugafuga.com | cross-site |
自社で作成しているシステムが SameSite 属性に引っかかった時の対策としては、SameSite 属性を None にして、かつ Secure 属性を付与させることで動作することが考えられます。
実践編
取り急ぎ、上記の内容について試していこうかなと思います。なお、今回は私がまだ慣れている Azure の App Service で確認しました。以下のような構成ですね。
用意する必要があるのは以下の 2 つです。
- Server A : HTML を表示させるサーバ
- Server B : Web API サーバ(node.js の restify を使用)(ただ html 呼ぶだけにしたのでもはや何でも良し!)
ちなみに、localhost を使わずにどっちも App Service でやったのはスキーマを http,https どっちもやろうと思った時にこれが個人的にてっとり早かったからです。
とにかくやってみる
ServerA に以下のような HTML を置きます。<ServerA> は App Service でいう "hogehoge.azurewebsites.net" みたいな感じでトップ ドメイン レベルまでを入れたら一緒のことを試せると思います。Server A については http と https の 2 通りをしてみます。
また、最後に Google Books API を比較対象として追加してます。
<html>
<body>
<br>
<button onclick="sendRequest('http://<ServerA>/samesite.html')">
Send Request to same-site(http)
</button>
</br>
<br>
<button onclick="sendRequest('https://<ServerA>/samesite.html')">
Send Request to same-site(https)
</button>
</br>
<br>
<button onclick="sendRequest('https://<ServerB>')">
Send Request to cross-origin
</button>
</br>
<br>
<button onclick="sendRequest('https://www.googleapis.com/books/v1/volumes?q=isbn:9784794221780&country=JP')">
Send Request to cross-origin(Google Books API)
</button>
</br>
<script>
const xhr = new XMLHttpRequest();
function sendRequest(url) {
xhr.open('GET', url);
xhr.send();
}
</script>
</body>
</html>
ちなみにコードとしてはボタンを onclick した時に XMLHttpRequest を用いてリクエストを送信するだけのシンプルなものです(もっと良い方法もあるかもだけどまあこれでいいや)
Server A の samesite.html や serverB のトップの html にはテキトーに以下な感じで HTML を置いておけばおk
<p>Hello from same-site/cross-site</p>
後は強いていうのであれば、Server B の構築には同じ構成にするなら以前に書いた App Service をお試しする記事を参考にするというか、もし暇で興味があれば読んでいただけたらなというくらいです。
リクエストを送りながら、Developer tool で見ていこうかなと思います。
ということで上から順に実行してみたらこうなりました。
1 つ目のリクエストについて
http でスキーマが違うことから CORS と認識されたみたい。(2 つ目との違いはスキーマだけ)
Mixed Content: The page at 'https://<serverA>/' was loaded over HTTPS, but requested an insecure XMLHttpRequest endpoint 'https://<serverA>/samesite.html'. This request has been blocked; the content must be served over HTTPS.
3 つ目のリクエストに関して
CORS のポリシー的にブロックされてるよって言われてますね。
Access to XMLHttpRequest at 'https://<serverB>/' from origin 'https://<serverA>/' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
ただ、4 つ目の GoogleBooks API は通ってそう。なので、ServerB 側(API を送られた App Service 側)の設定を確認してみよう。
それっぽいのがあった。
ので、一旦は全部許可してみよう。
これでもっかい動作確認してみる。
とりあえずはいけてそう :)
ServerA からのリクエストにオプションを追加してみよう
ここで少しServerA で HTML からのリクエスト送信時に withCredentials というオプションをつけてみます。
<script>
const xhr = new XMLHttpRequest();
function sendRequest(url) {
xhr.open('GET', url);
xhr.withCredentials = true;
xhr.send();
}
</script>
やってみた感じが以下。
ふむ、次ページ内での https には問題なさそうですが、CORS 設定を "*" にしていた自分の API サーバや GoogleBooks へのリクエストでエラーが出ましたね。
1 つ目のエラー : 自分の API Server(App Service) へのリクエスト
こちらについては CORS の設定を "*" から ServerA にすることで 2 つ目の GoogleBooks のエラーと同じになった。
2 つ目のエラー : GoogleBooks のへのリクエスト
Resoponse Header に属性が足りていないみたい。
とりあえず、HTML からのリクエスト先の Response に Headeer を追加してみよう。
ということで、ここからは自分の App Service へのリクエストに絞って確認してみます。
(同じサーバ内や GoogleBooks は保留)
API Server 側の Response に Header を追加
App Service で CORS 設定をしてた時に、なんかこんなのがあった。
なんかいけそうな気がしてきたのでダメ元でやってみた。
イケてそう(^^)
とりあえず、CORS 関連を色々と試せたので一旦ここまでとします。(SameSite 属性の方は別途検証してみますかね)
個人的なまとめ
感想
きっともっと良記事が世の中にはいっぱい出てるんでしょうけど、折角なので自分で手を動かして確認してみようということと、折角なのでなるべく他の人も試せるような感じで纏めてみようということでまとめてみました。もし Azure アカウント持ってる方がいらっしゃったら、Free で試せるような内容なので気が向いたら試してみても良いのではと思います。
学び
Service によってはCORSを補う機能を割と標準でつけてくれてる
CORS とか Access-Control-Allow-Credentials とかに関しては全部 App Service 側の設定で GUI ポチポチで出来ました。(気付かず結構 restify 側の Header を編集してみてたのは内緒の話)
ちなみに、ちらっと探してみた感じサーバアプリ側で実装しようと思ったらこのQiitaの記事が参考になるかなって思いました。
Cookie とかの Credential の使い時は考え物
例えば今回の Google Books API に関しては credential なしでも動作してそうでした。
なので、使う必要なさそうなので、そんな時はリクエスト送信時の credential の option を付けなければよい感じでしたね。(実際動いてた)
Discussion