💯

【GCS】WebアプリでCloudStorageの画像を安全に表示する

2024/10/05に公開

やりたいこと

GoogleCloudStorageにある画像を自Webアプリで表示する。
セキュリティ的にインターネット上で公開する際は"自アプリを通してのみ"見られるようにしたい。

構成と前提

自Webアプリ フロントエンド

  • FireBaseでホスティングにて公開済み
  • Rect × TypeScript

自Webアプリ バックエンド

  • herokuで公開済み
  • Go(gin)

ファイルストレージ

  • herokuはファイルを置けないのでGoogleCloudStorageで管理している
  • GCPのプロジェクト諸々は設定済み
  • Googleのサービスアカウント作成済み

対応策

  • 署名付きURLを使ってインターネットに公開する
  • 署名付きURLの生成にはGoogleのサービスアカウントのプリンシパルを使用する

Google Cloud Storage にある画像を認証済みURLを使って Web アプリで表示する際に、Google のサービスアカウントのプリンシパルを適用する。
そのためには、署名付きURL(Signed URL)を使用する。
署名付きURLを作成することで、指定した期間内に限り、特定のリソース(画像など)に対してアクセス権を付与できる。
このURLはサービスアカウントの認証を基に生成され、他のユーザーもそのURLを使ってリソースにアクセスできます。

署名付きURLの利点

  • 認証済みユーザーだけが、限られた期間内にリソースにアクセスでききる
  • バケットやオブジェクトに対するグローバルなアクセス権を変更する必要がなく、セキュリティが向上する
  • サービスアカウントを使ってバックエンド側で署名付きURLを生成するため、ユーザーが直接認証情報を扱う必要がない(これ嬉しい)

注意点

  • 署名付きURLは、生成後の有効期間が過ぎるとアクセスできなくなる。
    • ユーザーが直接ブラウザのブックマークに入れてしまわないような構成にしてあげる配慮がいる
  • 秘密鍵の管理は慎重に行う。
    • 署名付きURLに限った話ではない。(鍵だけに)
    • サービスアカウントの認証JSONファイルは安全な場所に保管する必要がある
    • herokuでのデプロイの際には工夫が必要。

やり方

署名付きURLをGoで生成する

署名付きURLを生成するためのコードをバックエンドに追加します。storage.SignedURL関数を使用してURLを生成します。
Goバックエンド

package main

import (
    "context"
    "fmt"
    "log"
    "time"
    "cloud.google.com/go/storage"
    "google.golang.org/api/option"
)

func generateSignedURL(bucketName, objectName, credentialFile string) (string, error) {
    ctx := context.Background()
    
    // ストレージクライアントを作成
    client, err := storage.NewClient(ctx, option.WithCredentialsFile(credentialFile))
    if err != nil {
        return "", fmt.Errorf("storage.NewClient: %v", err)
    }
    defer client.Close()

    // 署名付きURLのオプションを指定
    opts := &storage.SignedURLOptions{
        GoogleAccessID: "[YOUR_SERVICE_ACCOUNT]@your-project-id.iam.gserviceaccount.com", // サービスアカウントのメールアドレス
        PrivateKey:     []byte("[YOUR_PRIVATE_KEY]"), // サービスアカウントの秘密鍵
        Method:         "GET",
        Expires:        time.Now().Add(15 * time.Minute), // URLの有効期限
    }

    // 署名付きURLの生成
    url, err := storage.SignedURL(bucketName, objectName, opts)
    if err != nil {
        return "", fmt.Errorf("storage.SignedURL: %v", err)
    }

    return url, nil
}

func main() {
    bucketName := "your-bucket-name"
    objectName := "your-object-name"
    credentialFile := "path/to/your-service-account.json"

    signedURL, err := generateSignedURL(bucketName, objectName, credentialFile)
    if err != nil {
        log.Fatalf("Failed to generate signed URL: %v", err)
    }

    fmt.Printf("Signed URL: %s\n", signedURL)
}

  • GoogleAccessID: 署名付きURLを生成するために使用するサービスアカウントのメールアドレス。
  • PrivateKey: サービスアカウントの秘密鍵。この鍵は、サービスアカウントのJSONファイル内に含まれています。
  • Expires: 署名付きURLの有効期限を指定。ここでは15分間に設定しています。

Webアプリでの画像表示

上記のコードで生成した署名付きURLをフロントエンドに渡し、そのURLを使って画像を表示します。例えば、React で画像を表示する場合は以下のようにします。

Reactフロントエンド

import React, { useState, useEffect } from 'react';

const ImageComponent = () => {
  const [imageUrl, setImageUrl] = useState('');

  useEffect(() => {
    // バックエンドから署名付きURLを取得するリクエスト
    fetch('/api/get-signed-url')
      .then(response => response.json())
      .then(data => {
        setImageUrl(data.signedUrl);
      })
      .catch(error => {
        console.error('Error fetching signed URL:', error);
      });
  }, []);

  return (
    <div>
      {imageUrl ? <img src={imageUrl} alt="Your image" /> : <p>Loading...</p>}
    </div>
  );
};

export default ImageComponent;

  • /api/get-signed-url: バックエンド側で署名付きURLを生成し、フロントエンドに返すAPIエンドポイント。
  • imageUrl: 署名付きURLを使用して画像を表示。

Discussion