【.NET6】Google Cloud Storage から署名付き画像URLを取得する

# はじめに
GCSに格納した画像の署名付きURLの取得を実装しようとした際に、実装例が少なかったため記事に書き出そうと思います。


## 署名付きURLとは
署名付きURLとは認証情報や有効期限などの情報を含むURLです。  
URL自体に認証情報を含むため、認証情報を持たないユーザーでもリソースに特定の操作が可能となります。  
今回は画面上に画像を表示するために署名付きのURLを取得することで、画面にアクセス可能な人は誰でも画像が表示できる事を実現します。  
署名なしのURLや期限切れURLでは表示できません。  

## 実現したいこと
GoogleCloudStorage上にアップロードした画像の署名付きURLを取得して画面（Webページ）に表示する事を実現します。  
以下の画像のイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1119315/810b8116-6a96-4a40-41f6-4b950d0daba8.png)

<br>

# 事前準備

## GCSにバケットを追加
GCSにバケットを追加し、署名付きURLを取得したい画像を配置します。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1119315/f76348cb-64d8-7f42-fca3-3d734d535abb.png)

<br>

## サービスアカウントの追加
GoogleCloudStorageに接続し、署名を行うためのサービスアカウントを作成します。  
その際に最低限必要な権限は以下の権限です。
- storage.objects.get（GCSのオブジェクト参照権限）
- iam.serviceAccounts.signBlob（Blobの署名権限）

上記2つの権限を持つロールを作成し、サービスアカウントに割り当てました。
- 作成したロール  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1119315/b2caeecc-c0c3-d019-7b4b-a6dda87de67f.png)
- サービスアカウントへの割り当て  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1119315/61126db4-832d-8f44-c35f-02dba3f0b351.png)


## サービスアカウントキーの発行（ローカル実行確認用）
作成したサービスアカウントのキーファイルを発行します。  
今回はjson形式のキーファイルを発行しました。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1119315/c1d7fcab-3c3a-0562-1c9d-b332fef6a28d.png)

発行したキーはローカル上の任意の場所に置きます。  
ローカル実行時にこちらのサービスアカウントでアクセスできるよう、後ほど作成するWeb API プロジェクトの環境変数に以下の設定が必要になります。
```json
     "environmentVariables": {
        "GOOGLE_APPLICATION_CREDENTIALS": "{サービスアカウントキーファイルのパス}",
     }
```

<br>

# 手順
## 【手順1】パッケージのインストール
Web API プロジェクトを作成し、実装に使用する以下のパッケージをインストールします。  
- Google.Apis.Iam.v1
- Google.Cloud.Storage.V1
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1119315/adeb1a81-be6c-5968-b7bd-9c68c6f7c8cf.png)


## 【手順2】IamServiceBlobSignerクラスの作成
署名を行うためのBlobSignerをインスタンス化するために、IamServiceBlobSignerクラスを実装します。  
ソースコードは公式のページから引用し、今回用に少し変更しました。  
（記事末尾に参考ページを記載）

```c#
internal sealed class IamServiceBlobSigner : UrlSigner.IBlobSigner
{
    private readonly IamService _iamService;

    public string Id { get; }

    public string Algorithm => "GOOG4-RSA-SHA256";

    internal IamServiceBlobSigner(IamService service, string id)
    {
        _iamService = service;
        Id = id;
    }

    public string CreateSignature(byte[] data, UrlSigner.BlobSignerParameters _) =>
        CreateRequest(data).Execute().Signature;

    public Task<string> CreateSignatureAsync(byte[] data, UrlSigner.BlobSignerParameters parameters, CancellationToken cancellationToken)
    {
        throw new NotImplementedException();
    }

    private ProjectsResource.ServiceAccountsResource.SignBlobRequest CreateRequest(byte[] data)
    {
        SignBlobRequest body = new SignBlobRequest { BytesToSign = Convert.ToBase64String(data) };
        string account = $"projects/-/serviceAccounts/{Id}";
        ProjectsResource.ServiceAccountsResource.SignBlobRequest request =
            _iamService.Projects.ServiceAccounts.SignBlob(body, account);
        return request;
    }
}
```

## 【手順3】UrlSignerのインスタンス化
先の手順で作成したIamServiceBlobSignerを用いてblobSignerをインスタンス化し、続けてUrlSignerを生成します。  
今回はコンストラクタの中で実装しました。

```c#
public class CloudStorageService
{
    /// <summary>サービスアカウントID</summary>
    private string ServiceAccountId { get; set; }

    /// <summary>バケット名</summary>
    private string BucketName { get; set; }

    /// <summary>URL取得用</summary>
    private UrlSigner UrlSigner { get; set; }

    public CloudStorageService()
    {
        // ローカル実行では環境変数のGOOGLE_APPLICATION_CREDENTIALSが参照される
        var credential = GoogleCredential.GetApplicationDefault();
        BucketName = "{バケット名}";
        ServiceAccountId = "{サービスアカウントID}";

        // IamServiceClientObjectの作成
        credential = credential.CreateScoped(IamService.Scope.CloudPlatform);
        IamService iamService = new IamService(new BaseClientService.Initializer
        {
            HttpClientInitializer = credential
        });

        // 先の手順で作成した IamServiceBlobSigner を用いてUrlSignerの作成
        IamServiceBlobSigner blobSigner = new IamServiceBlobSigner(iamService, ServiceAccountId);
        UrlSigner = UrlSigner.FromBlobSigner(blobSigner);
    }
}
```

## 【手順4】画像URIの取得と署名
呼び出し時に署名を行いURLを取得するメソッドを作成します。  
また、このメソッドを呼び出す形でAPIを作成します。

```c#
/// <summary>
/// BLOBのURIを取得する
/// </summary>
/// <param name="blobName">BLOB名</param>
/// <returns></returns>
public Uri GetBlobUri(string blobName)
{
    // リクエストURLの作成
    UrlSigner.RequestTemplate requestTemplate = UrlSigner.RequestTemplate
        .FromBucket(BucketName)
        .WithObjectName(blobName)
        .WithHttpMethod(HttpMethod.Get);

    // 署名の有効期限などのオプションを設定
    UrlSigner.Options options = UrlSigner.Options.FromDuration(TimeSpan.FromHours(1));

    var url = UrlSigner.Sign(requestTemplate, options);
    return new Uri(url);
}
```
<br>

# 実行結果
起動してAPIをコールし、返却されたURLにアクセスすると無事に画像が表示できました。  
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1119315/e9e7ecb8-83b9-c06f-9b3b-6f40fba6274f.png)

<br>

# ソースコード
## Contoroller
```c#
using GetSignedBlobUrl.Services;
using Microsoft.AspNetCore.Mvc;

namespace GetSignedBlobUrl.Controllers
{
    [ApiController]
    [Route("api/getUrl")]
    public class GetUrlController : ControllerBase
    {
        private readonly ILogger<GetUrlController> _logger;

        public GetUrlController(ILogger<GetUrlController> logger)
        {
            _logger = logger;
        }

        [HttpGet("{name}")]
        public IActionResult GetUrl(string name)
        {
            var service = new GetUrlService();
            return new OkObjectResult(service.GetBlobUri(name));
        }
    }
}
```

## Service
```c#
using Google.Apis.Auth.OAuth2;
using Google.Apis.Iam.v1;
using Google.Apis.Iam.v1.Data;
using Google.Apis.Services;
using Google.Cloud.Storage.V1;


namespace GetSignedBlobUrl.Services
{
    public class GetUrlService
    {
        /// <summaryサービスアカウントID</summary>
        private string ServiceAccountId { get; set; }

        /// <summaryバケット名</summary>
        private string BucketName { get; set; }

        /// <summary>URL取得用</summary>
        private UrlSigner UrlSigner { get; set; }

        public GetUrlService()
        {
            GoogleCredential credential;

            // ローカル実行では環境変数のGOOGLE_APPLICATION_CREDENTIALSが参照される
            credential = GoogleCredential.GetApplicationDefault();
            BucketName = "{バケット名}";
            ServiceAccountId = "{サービスアカウントID}";

            // IamServiceClientObjectの作成
            credential = credential.CreateScoped(IamService.Scope.CloudPlatform);
            IamService iamService = new IamService(new BaseClientService.Initializer
            {
                HttpClientInitializer = credential
            });

            // 先の手順で作成した IamServiceBlobSigner を用いてUrlSignerの作成
            IamServiceBlobSigner blobSigner = new IamServiceBlobSigner(iamService, ServiceAccountId);
            UrlSigner = UrlSigner.FromBlobSigner(blobSigner);
        }

        /// <summary>
        /// BLOBのURIを取得する
        /// </summary>
        /// <param name="blobName">BLOB名</param>
        /// <returns></returns>
        public Uri GetBlobUri(string blobName)
        {
            // リクエストURLの作成
            UrlSigner.RequestTemplate requestTemplate = UrlSigner.RequestTemplate
                .FromBucket(BucketName)
                .WithObjectName(blobName)
                .WithHttpMethod(HttpMethod.Get);

            // 署名の有効期限などのオプションを設定
            UrlSigner.Options options = UrlSigner.Options.FromDuration(TimeSpan.FromHours(1));

            var url = UrlSigner.Sign(requestTemplate, options);
            return new Uri(url);

        }

    }

    /// <summary>
    /// 署名部分の実装
    /// </summary>
    internal sealed class IamServiceBlobSigner : UrlSigner.IBlobSigner
    {
        private readonly IamService _iamService;

        public string Id { get; }

        public string Algorithm => "GOOG4-RSA-SHA256";

        internal IamServiceBlobSigner(IamService service, string id)
        {
            _iamService = service;
            Id = id;
        }

        public string CreateSignature(byte[] data, UrlSigner.BlobSignerParameters _) =>
            CreateRequest(data).Execute().Signature;

        public Task<string> CreateSignatureAsync(byte[] data, UrlSigner.BlobSignerParameters parameters, CancellationToken cancellationToken)
        {
            throw new NotImplementedException();
        }

        private ProjectsResource.ServiceAccountsResource.SignBlobRequest CreateRequest(byte[] data)
        {
            SignBlobRequest body = new SignBlobRequest { BytesToSign = Convert.ToBase64String(data) };
            string account = $"projects/-/serviceAccounts/{Id}";
            ProjectsResource.ServiceAccountsResource.SignBlobRequest request =
                _iamService.Projects.ServiceAccounts.SignBlob(body, account);
            return request;
        }
    }
}
```

<br>

# 参考サイト
https://cloud.google.com/dotnet/docs/reference/Google.Cloud.Storage.V1/latest

<br>

# 関連記事
https://qiita.com/harunobu919/items/4574b40ff28d0f345201

https://qiita.com/nakazawaw/items/5602ee2337e8d3f92bff

https://qiita.com/nakazawaw/items/5cc58a2444a26667fcb2