Google Cloud Storageでブラウザからアップロードする by 署名付きURL
この記事では、ブラウザからアプリケーションサーバを挟まずにファイルをCloud Storageへアップロードする方法を見つけたので共有します。何卒何卒...
通常ではアプリケーションサーバを挟むことでアップロードに独自の処理(ログや認証など)を挟むことができ、「一定条件の場合はアップロードさせない」と言った処理を挟むことができます。
ですが、アプリケーションサーバを挟むことで通信が ①「ブラウザ->アプリケーションサーバ」 と ②「アプリケーションサーバ->Cloud Storage」 の2つになります。
アプリケーションのデプロイ先をVercelなどのような帯域幅(1回のリクエストのサイズ)に制限があるプラットフォームを選んでいると送信できるファイルに制限ができてしまいます。
これを回避するために ①「ブラウザ->アプリケーションサーバ」 の通信をなくしてCloudStorageに送信する、つまり 「ブラウザ->CloudStorage」 というルートでファイルをアップロードする
目標
- フロントエンドJSからファイルをCloudStorageへアップロードする
- あわよくば認証などのロジックを挟みたい
実装の全体像
今回実装するアップロード機能の概要は以下のとおりです。
手順:
- ブラウザはアプリケーションサーバに署名付きURL(ファイルのアップロード先)をリクエストする
- アプリケーションサーバは1のリクエストに対して署名付きURLを返す。もし認証などのロジックを挟みたければここで挟む。
- ブラウザは2のレスポンスで受け取ったURLに対してファイルを送信する。
よって今回実装すべき構成要素は以下のとおりです。
-
Google Cloud Storage
... ファイルを最終的にアップロードするバケット。 -
ブラウザ側のJS
... 以下の手順でファイルをアップロードする- アプリケーションサーバに署名付きURLをリクエスト
- 1で受け取った署名付きURLにファイルをアップロード
-
アプリケーションサーバ
... 署名付きURLをレスポンスするWebAPIを公開。リクエストを受け取った時に認証などの特定のロジックを挟む。
実装1 GoogleCloudStorage
サービスアカウント
ユーザなどの代わりに色々な操作を行うためのサービスアカウントを用意します。アプリケーションサーバで使用します。
サービスアカウントを作成する
-
サービスアカウントのページ へアクセス
-
サービスアカウントを作成するボタンをクリック
- サービスアカウント名を入力し、サービスアカウント作成を完了します。
ロールや権限ユーザ等は空白のままでOKです。(権限は後で追加します)
- キーファイルをダウンロードしておく
後で必要になるキーファイルをダウンロードしておきます。
バケット
次にアップロード先のバケットをGCPのコンソール等で作成しておきます。
バケットの作り方
- Cloud Storageにアクセス
- 上の方の作成から作成します。
バケット名などは適当でいいですが、リージョンによって発生する料金が微妙に異なりますので要注意です。
また公開アクセスは禁止しておきます。(デフォルトで禁止になってるはずですが)(公開したらアプリケーションサーバで認証する意味がなくなってしまう)
サービスアカウントに権限を与える
またサービスアカウントがバケットを操作できるようにサービスアカウントにCloudStorageを操作する権限を与えておきます。
権限の付与手順
-
先ほど作ったバケットを開きます
-
権限タブを開きます。
-
アクセス権を付与をクリック
以下のように入力します。
項目 | 入力値 |
---|---|
新しいプリンシパル | サービスアカウントのメールアドレス |
ロールを割り当てる | CloudStorage -> Storage オブジェクト管理者 |
これで選択したサービスアカウントで作成したバケットを操作することができるようになりました。
CORSの設定
今回はブラウザからアクセスしますが、Cloud StorageはデフォルトではCORSの設定がされていないため、CORSエラーが発生してしまいます。よってCORSの設定をします。
これを参考に設定してみてください。ちなみに作成するJSONファイルは次のような感じになるはずです。
[
{
"origin": ["http://hogehoge.com"], // ⚠️ hogehoge.com はlocalhost:3000など適宜変えてください
"method": ["PUT"], // ⚠️ ファイルアップロード時はPUTメソッドを使うのでPUT,参照時はGETだと思います
"responseHeader": ["Content-Type"],
"maxAgeSeconds": 3600
}
]
実装2 ブラウザ側JSの実装
素のJSでもReactでも何でもいいですが以下のような処理を画像をアップロードしたいタイミング(button.onclickやform.onsubmitなど)で実行します。(JSと言いながらTypeScriptで書いています)
const appServerUrl = "アプリケーションサーバのURL"
// 署名付きURLを取得
const signedGcsUrl = await fetch(appServerUrl).then(r=>r.json())
// 取得した署名付きURLにファイルを送信
await fetch(signedGcsUrl, {
method: "POST",
body: ファイルオブジェクト, // <input type="file"> から取得
})
実装3 アプリケーションサーバ
ここが今回のキモです。今回はNodeJSで実装しますが言語はGoogleCloudSDKが対応していればなんでもいいです。
import { Storage } from "@google-cloud/storage"
// ......
// ここで認証などのロジックを挟む
// アドレスにアクセスされた時に以下を実行
const GCP_PROJECT_ID = "プロジェクトID"
const storage = new Storage({
projectId: GCP_PROJECT_ID,
keyFilename: "サービスアカウントのキーファイルのパス",
})
const bucket = storage.bucket("バケット名")
const [signedGcsUrl] = await bucket
.file("ファイル名")
.getSignedUrl({ // 現在ログインしているアカウント(=サービスアカウント)で対象ファイルへ自由に書き込みできる権利を与える
version: "v4",
action: "write",
expires: Date.now() + 15 * 60 * 1000 , // 作成から15分
})
// signedGcsUrlを返す
res.json(signedGcsUrl)
見ての通りgetSignedUrl
というメソッドで簡単に署名付きURLを作成します。
署名付きURLについて
署名付きURLは電子署名技術により他者が改変不可能な文字列が含まれるスーパーCloudStorageのURLです。他社により改変不可能なので署名付きURLに含まれる署名を検証することでそのURLを生成したのが確実にサービスアカウントであることが保証できます。
Discussion