⚙️

Cloud StorageのCORSエラーを解決し、オブジェクトを署名付きURLでDLする

に公開

経緯

開発中のWebアプリで、バックエンドでCloud Storageオブジェクトの署名付きURLを取得し、それをフロントエンドで表示とダウンロード用のリンクとして使うような実装にしたところ、ダウンロードの時にCORSエラーが出てしまいました。
表示は問題なくできていたので「何故???」と悩むことになりましたが、解決方法さえわかればそんなに難しくなかったので、同じく困っている方がいれば参考にしてください。

対象者

  • 同じくCORSエラーで悩んでいる方
  • CORSって何?なんで表示はできるのにダウンロードできないの?って方

動作環境

バージョン
Mac(Apple M2) 14.7.6
PHP 8.3
gsutil 5.35

前提知識:CORSとは

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

ドキュメントめっちゃ長い&色んなページ読まないと分からないので、ポイントっぽいとこだけざっくりまとめます。
細かいところはドキュメントを読んで補ってください。
一旦読まなくていいやって方は、この章は読み飛ばしてください。

CORSは、Cross-Origin Resource Sharing(オリジン間リソース共有)の略称です。

オリジンは、Webコンテンツにアクセスするために使われるもので、URLのスキーム(プロトコル)ホスト(ドメイン)ポート番号によって定義され、この3つ全てが一致した場合のみ、2つのオブジェクトは同じオリジンであると言えます。
CORSを使うと、この制約を緩和することもできます。

CORSは、HTTPヘッダーベースの仕組みを利用して、あるオリジンで動作しているWebアプリに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザに指示するための仕組みです。

Webアプリは、自分とは異なるオリジンにあるリソースをリクエストするとき、オリジン間HTTPリクエストを実行します。
例えば、https://domain-a.comで提供されているWebアプリのフロントエンドJSコードが、fetch()を使用してhttps://domain-a.com/data.jsonにリクエストを送るイメージです。
CORSで通信すると、異なるオリジンにあってもアクセスできるようになるよーって感じです。

セキュリティ上の理由から、ブラウザはスクリプトによって開始されるオリジン間HTTPリクエストを制限しています
例えば、fetch()XMLHttpRequestは同一オリジンポリシーに従います。
つまり、これらのAPIを使用するWebアプリは、そのアプリが読み込まれたのと同じオリジンに対してのみリソースのリクエストを行うことができ、それ以外のオリジンからは正しいCORSヘッダーを含んでいる必要があるってことです。含んでないとCORSエラーになります。

なぜダウンロードの時だけエラーになるのか

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

https://cloud.google.com/storage/docs/cross-origin?hl=ja

Cloud Storageの仕様では、CORSをサポートするようにバケットを構成することが許可されています。

CORSリクエストには、「シンプル」と「プリフライト」の2種類があります。

シンプルリクエストはGET、HEAD、POSTメソッドを使っているかつ、特定のヘッダーのみで構成されているものを指し、こちらはCORSの設定無しで直接開始できます。
プリフライトリクエストは、メインリクエストの送信前に、サーバーに予備のリクエストとして送信され、権限を取得するものです。

ということで、ダウンロードの時だけエラーになるのは、「ダウンロードの実行にプリフライトリクエストが必要になるから」と言えそうです。
このプリフライトリクエストが不要になるようCORSの設定を変えると、CORSエラーは出なくなります。

本題

さて、ここからは具体的にどうやってCORSエラーを解決したのかをまとめます。
上手くいった方法と上手くいかなかった方法があるので、どちらも紹介します。

どちらの方法でも必須の項目

バケットのCORS構成の設定と表示には、そのバケットに対するストレージ管理者の権限が必須です。
これが設定できてないと、以下で紹介する手順は何もかもPermissionで弾かれます。

https://cloud.google.com/storage/docs/using-cors?hl=ja#required-roles

また、CORSの設定内容も決めておく必要があります。
こんな感じのやつです。

[
  {
    "origin": ["*"],
    "method": ["GET"],
    "maxAgeSeconds": 3600
  }
]

上記の例だと、originが対象のHosting URLで、methodが対象のHTTPメソッド、maxAgeSecondsが指定されたキャッシュの有効期間です。

上手くいかなかった方法:PHPからバケットの設定変更

PHPのコード(=アプリケーション側)で、バケットの設定を変更するためのメソッドを実行する方法を試しました。

以下、公式のサンプルコードです。
https://cloud.google.com/storage/docs/using-cors?hl=ja#configure-cors-bucket

これを任意の場所に追加したら、バケットに対してCORSの設定ができます。

こちらは、サンプルコードを参照しながら開発中のコードを調整してみたところ、確かに実行はできたんですが、以下のような問題点がありイマイチだと判断しました。

  • 連続で何度も実行していると、「limit for update」のエラーが出てしまう
  • メソッドを実行するためのサービスアカウントに、「ストレージ管理者」という非常に強い権限を与える必要がある

上手くいった方法:gsutilコマンドでの設定変更

CORSの設定ファイルを作成し、gsutilコマンドで設定する方法を試しました。
こちらの方が圧倒的にシンプルで簡単だったので、個人的にはこちらをオススメします。

ここでは、手っ取り早くCloud Shellを使って設定を反映するやり方を紹介します。
以下、手順です。

  1. Google Cloudコンソールにログインする

  2. Cloud Shellを起動する
    Cloud Shellの起動は、コンソール画面の右上にあるボタンを押すとできます。

  3. 「エディタを開く」ボタンをクリック

  4. 開いたエディタで、任意の場所にcors.jsonファイルを作成し、中に(CORSの設定内容)[#どちらの方法でも必須の項目]を記述する

  5. エディタを閉じて、以下のコマンドを実行する

gsutil cors set cors.json gs://<対象のバケット名>

こんな感じで設定できます。
めっちゃシンプルだし、すぐ反映できました。

設定が完了してから再度ダウンロードを試して、エラーが出なくなっていたら成功です。

参考資料

公式ドキュメント

https://developer.mozilla.org/ja/docs/Glossary/Origin
https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CORS
https://developer.mozilla.org/ja/docs/Web/HTTP/Guides/CORS/Errors

https://cloud.google.com/storage/docs/cross-origin?hl=ja
https://cloud.google.com/storage/docs/using-cors?hl=ja#client-libraries

技術記事

https://www.yukendev.com/blogs/gcs-cors
https://zenn.dev/taizo_pro/articles/b968ebbf0ed42d

カラビナテクノロジー デベロッパーブログ

Discussion