💰

CORSとブラウザキャッシュの問題

2023/03/12に公開

CORSの基本

CORS(Cross-Origin Resource Sharing)は異なるオリジン間でのリソースのアクセス権をブラウザに与えるための仕組みである。
セキュリティの上の理由でブラウザスクリプトによる別オリジンのリソースに対するアクセスは制限されているので、サーバー側からCORSに関するHTTPヘッダーを返す事によって、オリジン間のリソースへのアクセスを許可できる。

https://developer.mozilla.org/ja/docs/Web/HTTP/CORS

同一リソースへのアクセスとブラウザキャッシュの問題

主に画像を扱うときなどで、HTML側のimg要素から画像を読み込み、JavaScriptでもFetch APIなどで画像を取得するといったことはよくあると思われる。
そういった場合に異なるオリジンのリソースを取得するときは、サーバー側のレスポンスで然るべきCORSの対応を行えば問題ないが、ある特定の状況でCORSのレスポンスの設定が正しいにも関わらず、CORSエラーが発生するということがある。
それは、同じリソースに対してCORSを利用しないリクエストを送信したあとに、CORSを利用したリクエストを送信すると、CORSを利用したリクエストが失敗するという現象である。

実際にこの現象を確認するためにJavaScriptで以下のような関数を用意する。

const IMAGE_URL = "画像のURL"

function loadImage() {
  const image = new Image();
  image.src = IMAGE_URL
}

function loadImageWithCors() {
  const image = new Image();
  image.crossOrigin = "anonymous";
  image.src = IMAGE_URL
}

loadImageはImageコンストラクタでHTMLImageElementを生成して、srcから画像を読み込む関数。
loadImageWithCorsもやっていることはほぼ同じだが、こちらはcrossOriginプロパティにanonymousを設定する。
これにより、このインスタンスから画像を読み込む際のリクエストのヘッダーにOriginヘッダーが追加されるので、CORSなリクエストになる。

ボタンクリックのイベントでそれぞれの関数が実行できるようにする。
Chromeでページを開いてloadImage() -> loadImageWithCors()の順番で関数を実行すると以下のように2回目のリクエストはCORSエラーで失敗する。

画像

なぜ、このようなことが起こるかというとブラウザのHTTPキャッシュが原因である。
ブラウザには、HTTPリクエストをキャッシュして同一のリクエストが発生した場合はキャッシュしたレスポンスを使用することがある。
今回は最初にCORSではないリクエストを送信したことにより、CORSのヘッダーを含まないレスポンスをキャッシュ、次のOriginヘッダーを含むリクエストでキャッシュされたCORSのヘッダーをレスポンスを利用したため、
CORSエラーが発生したと考えられる。
なので、逆にloadImageWithCors() -> loadImage()の順番の場合、CORSのヘッダーを含むレスポンスをキャッシュするため、問題なくリソースを読み込める。
もちろん、ブラウザキャッシュを無効にした状態でもこの現象は起こらない。

ブラウザ間での動作の違い

ここまではGoogle Chromeでの動作を前提とした話だったが、他のブラウザでは少し挙動が違っているようである。

特にSafariの挙動が厄介で、SafariではCORSなレスポンスはキャッシュされず、非CORSなレスポンスはキャッシュされている。
すなわち、同じリソースに対して一度でも非CORSなリクエストを送信すると、それ以降CORSなリクエストがエラーになってしまう。
Chromeでは最初にCORSのレスポンスをキャッシュすれば、それ以降のリクエストに対してはそのキャッシュが利用されるので、リクエストの順番に注意すればCORSエラーを回避できるが、同じ実装でもSafariではCORSエラーが発生するということが起こりうる可能性がある。

ちなみにFirefoxではCORSリクエスト、非CORSリクエストをそれぞれ別にキャッシュしているようでどんな順番でリソースを読み込んでも問題ないことを確認した。

対策

以上のことを踏まえると、以下の対策が考えられる。

すべてのリクエストをCORSにする

XMLHttpRequestやFetch APIでのリクエストが発生しうるリソースがあるのであれば、すべてのリクエストをCORSにしてしまえばいいという考え方。
具体的には、CORSでリクエストされる可能性のあるリソースを読み込んでいるimg要素にcrossOrigin属性を付与する。

<img src="画像のURL" crossOrigin="anonymous" />

crossOrigin属性の詳しい説明に関しては以下のページを参照
https://developer.mozilla.org/ja/docs/Web/HTML/Attributes/crossorigin

ブラウザでのキャッシュを無効にする

ブラウザのキャッシュで問題が発生するのであれば、ブラウザのキャッシュを無効化すればいいという考え方。
HTTPヘッダーであるCache-Controlヘッダーはキャッシュをキャッシュの制御を支持できる。
今回であればリソースのレスポンスヘッダーにCache-Control: no-cacheを設定すればよい。

Cache-Controlの詳しい説明に関しては以下のページを参照
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Cache-Control

まとめ

同一リソースに対して非CORSリクエストとCORSリクエストが混在する場合は注意が必要である。
ブラウザによってはキャッシュにより、想定しないCORSエラーが発生しうることがある。
対策としてはすべてのリクエストをCORSにするか、キャッシュを無効にするかなどが考えられる。

Discussion