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

## やりたいこと
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でのデプロイの際には工夫が必要。
        - これに関しては神記事がある↓
        - https://qiita.com/YoshiYamamura/items/e478ca4d50e34ea7ccf4

## やり方

#### 署名付きURLをGoで生成する
署名付きURLを生成するためのコードをバックエンドに追加します。storage.SignedURL関数を使用してURLを生成します。
Goバックエンド
```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フロントエンド
```jsx
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を使用して画像を表示。