日が経過しています。情報の鮮度にご注意ください
本文の経緯
あるオンライン記事でCORSについて調べていた際、『withCredentials = trueを設定すると、送信されるcookieはターゲットドメインのcookieである』という一文に困惑しました。私は理解できませんでした:現在のドメインがa.comで、xhrをb.comに送信する場合、当然ソースドメインa.comのcookieをb.comに送信して処理するはずです。どうしてターゲットドメイン(ここではb.comと解釈)のcookieが送信されるのでしょうか?そこで調査を開始しました(結論から言うと、参照した資料の記述は正しく、実際にターゲットドメインb.comのcookieが送信されていました)。
小目標-1: シンプルリクエストでa.comからb.comへajaxを送信する
シンプルリクエストの定義については各自でGoogle/SOを参照してください。ここで重要な基本事実として、cookieはIPアドレスではなくドメインに紐づきます。私はローカルでcookieを処理できる簡単なexpressサービスを立ち上げ、同時にVPS上にも同じサービスを用意し、hostsファイルを編集して異なるドメインを実現しました:
1 | |
まずVPSサーバー上でexpressサービスを起動します。特に設定はせず、単純にheaderを返すだけです。ポートは9091で起動:
1 | |
a.comのサーバー側もほぼ同じですが、ajaxを送信するための静的ページを追加しています。ポートは9090で起動:
1 | |
index.htmlの内容:
1 | |
サーバー側でAccess-Control-Allow-Originを設定していないため、エラーが発生します:

次にb.comのサーバー側でa.comからのajaxを許可する設定を追加します(ポートまで正確に指定する必要があります):
1 | |
再度リクエストを送信:


コンソールにエラーが表示されず、ステータスコードが200であることから、b.comがa.com:9090からのリクエストを許可していることが確認できます。
小目標-2: ローカルサービスがフロントエンドからのcookieを受信する
まずローカルでa.comのバックエンドがフロントエンドからのcookieを取得できるかテストします:
テスト方法は簡単で、jsに適当なcookieを追加するだけです:
1 | |

ローカルコンソールのApplicationタブでcookieが存在することを確認できます。次にバックエンドの出力を確認:

問題ありません。www.a.com:9090にアクセスした際に確実にcookieが付与されています。予想通りです。
小目標-3: a.comのcookieをb.comに送信する
この時点でa.comのページにはcookieが存在します。そこで再度ボタンをクリックし、ajaxリクエストでcookieがb.comに送信されるか確認します:

cookieを追加しなかった場合と同様に、a.comからのcookieは取得できません。これはセキュリティ制限によるもので、予想通りの結果です。
追加の小目標: 非シンプルリクエスト
ここでシンプルリクエストに関するテストを挿入します。xhrに新しいheaderを追加し、再度リクエストを送信します:
1 | |
今回はAccess-Control-Allow-Originを追加した後の操作なので、ブラウザは異なるエラーを返します:

気づいたのはやはり Access-Control-Allow-Origin のエラーでしたが、今回はフロントエンドでカスタム header を設定していたため、非シンプルリクエストとなっていました。非シンプルリクエストの場合、まずプリフライトリクエスト(prelight)が送信され、リクエストタイプは OPTIONS です。詳細はこの記事](リンク)をご覧ください。プリフライトリクエストの目的は、b.com のサーバーがこの xiaodan という header を受け入れるかどうかを確認することです。バックエンドが返す header の Access-Control-Alow-Headers に、xiaodan という値が含まれていなかったため、エラーが発生しました。
次に、b.com が返す内容に適切な header を追加してみます:
1 | |
再度リクエストを送信してみます:

なんと同じエラー結果です。サーバーは 200 を返していますが、適切な Access-Control-Allow-Headers を返していないため、ブラウザが結果を拒否しました(サーバーが拒否したわけではなく、サーバーは 200 を返しています)。
調査してみると、問題はこの非シンプルリクエストにあることがわかりました。b.com の関数を少し修正します:
1 | |
サーバー側:

クライアント側:

原因を分析すると(確認待ち、後で HTTP の権威あるガイドを参照します)、非シンプルリクエストの prelight リクエストは実際のリクエストを送信せず、まずプリフライトリクエストを送信して、サーバーが特定の非シンプル header フィールドをサポートしているかどうかをテストします。つまり、非シンプルヘッダーを含むリクエストは app.get('/') の中には到達しません。同時に、b.com のサーバーでは、console.log(req.headers) が app.get('/') の中に書かれているため、先ほどのリクエストでは b.com サーバーは何も出力していません。これもこの点を裏付けています。この設計は、CORS標準を理解していない古いサーバーを保護するために、サーバーがCORS標準を認識していることを確認することを目的としています。
小目標-4: a.com ドメインの cookie を b.com に送信する
OK、中断はここまで。クライアント側、つまり a.com で発行される xhr リクエストのページで withCredentials = true を設定(xhrSend 部分のみを記載)して、a.com の cookie を b.com に送信できるかテストします(ここではシンプルリクエストと非シンプルリクエストは同じ結果です。違いを確認しやすくするために、xhr で設定した header を削除しました):
1 | |

今回のエラーメッセージが変わり、サーバーが Access-Control-Allow-Credentials を true に設定していないという内容になりました。この header はリクエストに cookie を含めることを許可するためのものです。そこで設定を追加します:
1 | |

まだありません。Chrome の cookie を確認します:

確かに cookie は設定されているはずです。どういうことでしょうか? 納得できず、req.headers が express でフォーマットされたものなので、生の headers である rawHeaders を確認します:

まだ見つかりません。cookie はどこに行ったのでしょうか?
どちらもない、彼女は将来見つけると言った、時間、時間が私に答えをくれる ------私のスケート靴
問題が見つからない時は、アイスクリームを食べましょう。階下でパクチー味のネスレアイスを買って(ハーゲンダッツは高くて手が出ません)、階段を上っている時にひらめきました。もしかしてこれはサードパーティの cookie で、ブラウザのトラッキングをブロックしているせいかもしれません。アイスを食べ終わった後、Chrome の設定で「“Do Not Track” リクエストをブラウジングトラフィックと共に送信する」のチェックを外しました:

ちなみに、今回は万が一に備えて(実際には万が一はありませんが)req.headers を app.use の中に置きました:
1 | |
再度ボタンをクリックしてリクエストを送信し、Chrome のコンソールと b.com のサーバー出力を確認します:
非シンプルヘッダーがあるため、前回と同様に2つのリクエストが表示されます:


サーバー側でも受信されていないため、この Chrome 設定とは無関係です。変数を制御するため、「Do Not Track」は前回と同じように再度チェックを入れました。サーバー側(GET リクエストのみ):

それでも Cookie フィールドがありません。なぜでしょうか?
記事の冒頭で述べた言葉を思い出しました: 「注意: withCredentials = true を設定すると、送信される cookie はターゲットドメインの cookie です」。もしかして、a.com からボタンをクリックして b.com にリクエストを送信する場合、b.com に設定された cookie が送信されるのでしょうか?
そこでまず、b.com のサーバーにも index.html を作成し、適当な cookie を設定してみます:
b.com のサーバーコード:
1 | |
b.com の index.html コード:
1 | |
OK、まず b.com にアクセスします:

問題ありません。正常にページが返され、cookie も設定されます。次に、a.com からボタンをクリックしてリクエストを送信します:

a.com から送信された ajax リクエストに、b.com の cookie が含まれていることが確認できます。
記事の冒頭の言葉が実証されました。
小目標-5: a.com の JavaScript で b.com の cookie を取得する:
a.com から b.com の cookie を送信できるなら、フロントエンドで b.com の cookie を取得できるでしょうか?
ドキュメントを確認すると、ajax には getAllResponseHeaders() と getResponseHeader() という2つのインターフェースがあり、サーバー側には Access-Control-Expose-Header があります。そこでテストしてみました(あえて分けて出力します)。
まずは簡単な方から、xhr の getAllResponseHeaders() インターフェースを呼び出します:
1 | |

header の Cookie フィールドが表示されませんでした。予想通りです。もしかして getAllResponseHeaders() で header を走査しても Cookie が表示されないのは、enumerable: false に設定されているからかもしれません。そこで getResponseHeader() も試してみました:
1 | |

これも予想通りでした。サーバー側で公開する header コンテンツが設定されていないためです。そこで b.com で Access-Control-Expose-Header を設定しました:
1 | |
再度 getResponseHeader('Cookie') と getAllResponseHeaders() を実行する

エラーはなくなったが、それでも b.com の cookie を取得できません。b.com のサーバー側がすべて許可していてもダメです。
調べてみると、この Access-Control-Expose-Header はカスタム header にしか設定できず、フロントエンドで取得できるようになるだけだとわかりました。しかし、これはあまり意味がありません。なぜなら、このカスタム header はフロントエンドで設定したもので、唯一の用途はバックエンドがカスタム header を変更/新規作成した後にフロントエンドが取得することだからです。以下では、バックエンドに header を設定し、フロントエンドで取得できるようにします:
b.com の server:
1 | |
a.com の index.html:
1 | |

この通りです。
したがって、この小さな目標は達成できませんが、SO コミュニティではサードパーティサービスを利用したりバックエンドで転送したりするなどの解決策が提案されています。結局のところ、ルールは固定的ですが、人間は柔軟に対応できます。jsonp と同じように、ですよね。
連想
「数ヶ月のトレーニングでゼロからある言語をマスターできる」と言う人がいますが、私は夢物語だと思います。なぜなら、コンピュータの基礎知識がなく、バイナリ/コンパイル原理/コンピュータ原理/OS原理/ネットワーク基礎/通信プロトコルが何なのかわからない状態でコードを書けるということは、単に模倣する学習能力が高いだけで、そう書くとそうなることは知っていても、なぜそう書くとそうなるのかは理解していないからです。
したがって、コンピュータと向き合うときは、知識の幅が広ければ広いほどよく、深ければ深いほど良いのです。このように、CORS を理解しているうちに、以前触れた AUTH を思い出しました。
AUTH2.0 というものがありますが、それが CORS と何の関係があるのでしょうか?実は関係ありませんが、以下は私の比較です:
CORS は、B サイトにアクセスしたユーザーが、A サイトからリクエストを送信する際に B サイトの cookie を携帯できるようにします。手順は次のとおりです:
- ユーザーが
Bサイトにアクセスする。 - ユーザーが
Aサイトにアクセスし、AサイトからBサイトにリクエストを送信する。 Bサイトは、Aサイトからのリクエストに含まれるBサイトのcookieを検証し、問題がなければBサイトのデータを返す。
AUTH は、ユーザーが A ウェブサイトから B ウェブサイトのリソースにアクセスできるようにします。ただし、ユーザーの承認が必要です。手順は次のとおりです:
Aウェブサイトでリクエストを送信する。- ステップ 1 で
Bサイトにリダイレクトされ、承認を確認した後、Aウェブサイトに戻る。 - この時点で
Aウェブサイトはtooken(トークン)を取得し、ユーザーのBウェブサイトのリソースにアクセスできるようになる。
似ていませんか?
CORS の最初のステップは AUTH の 2 番目のステップに対応します。AUTH の 2 番目のステップは、CORS の最初のステップと見なすことができ、B ウェブサイトのサーバー側に Access-Control-Allow-Credentials を追加したと考えることができます。これにより承認が完了したことになります。
CORS で cookie を携帯するのは、AUTH の簡易版と考えることができます。
以下は RFC 6749 からの抜粋です:

後書き
なぜサードパーティ広告のcookieがプライバシーを漏洩すると言われるのでしょうか?これは、広告がAというウェブサイトに掲載されると、広告主はその広告がAサイトに配信されたことを知る(広告配信のid/keyなどのidentityで識別し課金される)ためです。そこで広告主はこの広告にcookieを設定します。この広告は広告Bのドメインから提供されるため、設定されるcookieは当然Bのcookieになります。Aサイトがこの広告を読み込むたびに、必ずjsを実行します。このjsの中で、Aサイトがcookieを送信することを許可し、同時にBサイトもAサイトからのcookieにBサイトのcookieを添えて送信することを許可します。これにより、すべてが把握されてしまうのです。
`Google AD Impl:


注意事項
-
上記の変更にはサーバー側の修正が含まれており、いずれもサービスの再起動が必要です。
VPS上でサービスを再起動するのは不便であり、時間が経つと接続が切れてしまうため、最善の方法はVPS上に記事中のa.com(主にindex.htmlを修正するため)を配置し、ローカルに記事中のb.comの内容(主にindex.jsを修正するため)を配置することです。 -
リモート接続を維持する最も簡単な方法は、それをバックグラウンドで実行することです(接続を維持しないと、接続が切れた後にリクエストが来て
I/O操作を行っても、やはり切断されます)。node index.js &を実行するか、すでにnode index.jsを実行している場合はctrl+zを押してバックグラウンドで凍結し、bgコマンドで最新のバックグラウンドタスクをアクティブにします。jobsコマンドで現在のタスクリストを確認できます。現在のsessionから退出した後、再度接続するとタスクはまだ実行中ですが、jobsではそのタスクが見えなくなるため、ps -Aですべてのプロセスをリストアップし、kill IDでnodeのプロセスを終了させてから再実行します。 -
小目標の達成中にエラーが発生した際、カスタムフィールドを
X-で始めなかったためにエラーになったのではないかと疑いましたが、標準を確認したところX-で始めるという規定はなく、ウィキペディアやSOではカスタムHeaderをX-で始めることを推奨しているだけでした。 -
withCredentials = trueを設定した後は、サーバー側のAccess-Control-Allow-Originをwildcardの*に設定することはできなくなります。
人生の重要な選択に直面したとき、最善の方法を誰かが教えてくれて、貴重な時間を無駄にせずに済めばと、私はよく願っています。だからこそ、自分の経験を踏まえて頻繁にブログを書き、広大なインターネットのこの小さな片隅に、私にとって一度きりの人生経験を記録し、助けを求める人々の力になれればと思っています。