📖

Google Cloud Storageでブラウザからアップロードする by 署名付きURL

2023/01/25に公開

この記事では、ブラウザからアプリケーションサーバを挟まずにファイルを Cloud Storage へアップロードする方法を見つけたので共有します。何卒何卒...

通常では画像のアップロードなどにアプリケーションサーバを挟むことでアップロードに独自の処理(ログや認証など)を挟むことができ、「一定条件の場合はアップロードさせない」と言った処理を挟むことができます。

ですが、アプリケーションサーバを挟むことで通信が ①「ブラウザ->アプリケーションサーバ」 と ②「アプリケーションサーバ->Cloud Storage」 の 2 つになります。
アプリケーションのデプロイ先を Vercel などのような帯域幅(1 回のリクエストのサイズ)に制限があるプラットフォームを選んでいると送信できるファイルに制限ができてしまいます

これを回避するために ①「ブラウザ->アプリケーションサーバ」 の通信をなくして CloudStorage に送信する、つまり 「ブラウザ->CloudStorage」 というルートでファイルをアップロードする

目標

  • フロントエンド JS からファイルを CloudStorage へアップロードする
  • あわよくば認証などのロジックを挟みたい

実装の全体像

今回実装するアップロード機能の概要は以下のとおりです。

手順:

  1. ブラウザはアプリケーションサーバに署名付き URL(ファイルのアップロード先)をリクエストする
  2. アプリケーションサーバは 1 のリクエストに対して署名付き URLを返す。もし認証などのロジックを挟みたければここで挟む。
  3. ブラウザは 2 のレスポンスで受け取った URL に対してファイルを送信する。

よって今回実装すべき構成要素は以下のとおりです。

  • Google Cloud Storage ... ファイルを最終的にアップロードするバケット。
  • ブラウザ側のJS ... 以下の手順でファイルをアップロードする
    1. アプリケーションサーバに署名付き URL をリクエスト
    2. 1 で受け取った署名付き URL にファイルをアップロード
  • アプリケーションサーバ ... 署名付き URL をレスポンスする WebAPI を公開。リクエストを受け取った時に認証などの特定のロジックを挟む。

実装 1 GoogleCloudStorage

サービスアカウント

ユーザなどの代わりに色々な操作を行うためのサービスアカウントを用意します。アプリケーションサーバで使用します。

サービスアカウントを作成する
  1. サービスアカウントのページ へアクセス

  2. サービスアカウントを作成するボタンをクリック

  1. サービスアカウント名を入力し、サービスアカウント作成を完了します。

ロールや権限ユーザ等は空白のままで OK です。(権限は後で追加します)

  1. キーファイルをダウンロードしておく

後で必要になるキーファイルをダウンロードしておきます。

バケット

次にアップロード先のバケットを GCP のコンソール等で作成しておきます。

バケットの作り方
  1. Cloud Storageにアクセス

  1. 上の方の作成から作成します。

バケット名などは適当でいいですが、リージョンによって発生する料金が微妙に異なりますので要注意です。

また公開アクセスは禁止しておきます。(デフォルトで禁止になってるはずですが)(公開したらアプリケーションサーバで認証する意味がなくなってしまう)

サービスアカウントに権限を与える

またサービスアカウントがバケットを操作できるようにサービスアカウントに CloudStorage を操作する権限を与えておきます。

権限の付与手順
  1. Cloud Storageを開く

  2. 先ほど作ったバケットを開きます

  3. 権限タブを開きます。

  4. アクセス権を付与をクリック

以下のように入力します。

項目 入力値
新しいプリンシパル サービスアカウントのメールアドレス
ロールを割り当てる 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 で書いています)

ブラウザ側のJS
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