0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rust言語で署名付きURLによりGCSにファイルをアップロードする

Last updated at Posted at 2022-04-24

GCS(Google Cloud Storage)へファイルアップロードについて、仕組みの理解とRust言語でそれを実装する時にはまったことをメモする。

1. Google Cloud Storage 基本概念

1-1. バケット

1-1-1. バケットとは
  • ファイルをフォルダを格納するロジカル的なストレージ領域である。
    ローカルPCだと、ドライブ名的なもの。(WindowsシステムのCドライブ、Dドライブ)

  • アクセス方法:GCP > Cloud Storage > ブラウザ > バケット名をクリック

1-1-2. オブジェクト

不正確かもしれないが、バケット中のファイル名やフォルダ名のことを思えばよい。

1-1-3. 権限
  • 当たり前、基本はバケットは非公開
  • プリンシパル・ロールで強力的に色々コントロールできる印象

1-2. サービスアカウント

  • GCPの普通のユーザと違って、バックエンドAPIで利用するユーザのこと。
  • プライベート鍵JSONファイルをダウンロードして、署名付きURL作成時に利用する

Google Cloud Platformサービスアカウントを作成する

1-3. 署名付きアップロードURL

  • 下記情報が含まれる

    • アップロード先のバケットとオブジェクト名
    • http通信メソッド名(PUTかPOST)
    • http通信コンテンツタイプ(application/pdfなど)
  • サービスアカウントのプライベート鍵で暗号化する

    • サービスアカウント名や暗号化アルゴリズムなどをURLのリクエストパラメータに追加

1-4. GCS側デフォルトで行う署名内容の確認(Validator)

  • 指定サービスアカウントの公開鍵で認証する
  • http通信メソッドなど一致しているかをチェック
  • チェックOKだったらアップロードが行われる

1-5. 対象バケットをサービスアカウントのアクセスを許可する

次のどちらも行ける

1-5-1. サービスアカウントで広い範囲を設定
  • Cloudストレージ管理者権限を与えば、全てのGCSをアクセスできてしまう
    • バケットごとに細かく設定が不要になるメリットがある
    • 広すぎで、やられたら、影響範囲が広いデメリットが大きい
      • 管理者権限を与えるための権限が必要で、上長の承認を通らないいけないので、諦める
1-5-2. バケット画面の権限・プリンシプルで該当サービスアカウントの権限を追加

バケットの権限の設定

2. アーキテクチャー

architecture_with_GCP_services.max-1100x1100.max-900x900.png

  • フロントからバックエンドの署名付きURL作成apiを叩く
  • 取得されたURLへファイルをアップロードする

3. Rustで署名付きURL作成APIとはまったこと

3-1.軽量クレートtame_gcsを利用

  • 下記はactix-webフレームワークを利用してcontroller側の実装である
  • GCS_SERVICE_ACCOUNT_JSON_FILE環境設定には実際のプライベート鍵JSONファイルのパスが設定される
extern crate diesel;
use dotenv::dotenv;
use std::env;
use serde::Serialize;
use serde::Deserialize;

use actix_web::HttpResponse;
use actix_web::web;
use tame_gcs::{signed_url, signing, types};
use http;


#[derive(Serialize)]
struct GoogleCloudStorageUrl<'a> {
    signed_file_upload_url: &'a str,
}

#[derive(Deserialize)]
pub struct Param {
    file_name: String,
    content_type: String,
}

fn signed_url(file_name: &str, content_type: &str) -> Result<url::Url, tame_gcs::Error> {
    dotenv().ok();

    let gcs_service_account_json_file = env::var("GCS_SERVICE_ACCOUNT_JSON_FILE").expect("GCS_SERVICE_ACCOUNT_JSON_FILE must be set");

    let sa = signing::ServiceAccount::load_json_file(gcs_service_account_json_file)?;

    let mut headers = http::HeaderMap::default();
    headers.insert(http::header::CONTENT_TYPE, http::HeaderValue::from_str(content_type).unwrap());
    // let options = signed_url::SignedUrlOptional::default();
    let options = signed_url::SignedUrlOptional {
        method: http::Method::PUT,
        duration: std::time::Duration::from_secs(60 * 60),
        headers,
        region: "auto",
        query_params: Vec::new(),
    };
    let bucket = types::BucketName::try_from("hoge-upload-test")?;
    let object = types::ObjectName::try_from(file_name)?;
    let url_signer = signed_url::UrlSigner::with_ring();
    let signed_url = url_signer.generate(&sa, &(&bucket, &object), options)?;
    Ok(signed_url)
}

pub async fn file_upload_url(query: web::Query<Param>) -> HttpResponse {
    let file_name = query.file_name.as_str();
    let content_type = query.content_type.as_str();
    println!("file_name:{}", file_name);
    println!("content_type:{}", content_type);

    let url = signed_url(file_name, content_type).unwrap();
    println!("url:{}", url.as_str());

    HttpResponse::Ok().json(GoogleCloudStorageUrl {
        signed_file_upload_url: url.as_str(),
    })
}

Cargo.toml依頼の抜粋

[dependencies]
actix-web = "4.0.1"
actix-rt = "2.7.0"
tame-gcs = { version = "0.11.3", features = ["signing"] }
url = "2.2.2"
http = "0.2"

3-2. はまったこと

この記事を参考してすぐに署名付きURLができましたが、それを使ってアップロードすると、理由不明で403エラーになった。

  • chromeのコンソールにはなぜかCORSエラーと403が同時に表示される
    • CORSエラーがGCSかchromeのバグっぽい、それも目が惹かれってしまった
    • 具体的なレスポンスが何もないため、原因究明は難航
  • Firefoxならエラーレスポンスがちゃんと見れた。なるほど、SignatureDoesNotMatch署名が一致しないことでした。
<?xml version='1.0' encoding='UTF-8'?><Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your Google secret key and signing method.</Message><StringToSign>GOOG4-RSA-SHA256
20220421T163054Z
20220421/auto/storage/goog4_request
c87e06a70044bf8632a035fa36def8e8804085a76bc15574bc95a5f1241bc0d4</StringToSign><CanonicalRequest>POST
/hoge-upload-test/test.pdf
X-Goog-Algorithm=GOOG4-RSA-SHA256&amp;X-Goog-Credential=cloud-storage-account-01%40hogehoge-stg.iam.gserviceaccount.com%2F20220421%2Fauto%2Fstorage%2Fgoog4_request&amp;X-Goog-Date=20220421T163054Z&amp;X-Goog-Expires=3600&amp;X-Goog-SignedHeaders=host
host:storage.googleapis.com

host
UNSIGNED-PAYLOAD</CanonicalRequest></Error>
  • 原因判明

上記Qiita記事はアップロードためのURL作成ではないからだ。

  • デフォルトのSignedUrlOptionalを使ってGETための署名付きURLになる
  • GCS側でValidatorする時に、実際はPUTなので、当然通らない
let options = signed_url::SignedUrlOptional::default();
  • 修正内容

ネット記事Google Cloud Storage】署名付きURLによる画像のアップロードをみると、http method名やcontent-typeなども設定する必要があるようで、デフォルトをやめてSignedUrlOptionalを生成したら、上手くいった。

4. フロント側の実装とCORSエラーの対処

フロント側がaxiosを使ってファイルアップロード通信を行なうところ、GCSと自サービスのドメインが違うためCORSエラーが発生する。
それを通るには、違うドメインでもいいよとフロント側通信設定とGCSサーバ設定が必要。

4-1. axiosの設定

  • Access-Control-Allow-Originをaxiosのdefaults設定に追加
    • 下記はputでアップロードする場合の設定
    • axios通信メソッドのその場のheader設定が上手くいかない
axios.defaults.headers.put['Access-Control-Allow-Origin'] = '*';
  • 1つのファイルをアップロードする場合は、FormDataを使わない方良い

Google Cloud Storageの署名付きURLに対してaxiosでPUTするときにFormDataを使おうとしてはならない

  • content-typeがアップロードファイルのタイプである
    • pdfファイルなら「application/pdf」

4-2. GCSサーバ設定

gsutilsコマンドラインで行なう。(WEB画面で設定するかと思ったが、できなさそう。)

gsutil ツール

  • cors設定ファイル(cors_setting.json)を用意
[
    {
      "origin": ["*"],
      "responseHeader": ["*"],
      "method": ["*"],
      "maxAgeSeconds": 3600
    }
]

methodやresponseHeaderは必要によって後で変更でOK。

  • gsutilsコマンドで対象バケットのCORSを設定
gsutil cors set cors_setting.json gs://hoge-upload-test
  • CORS設定内容の確認
gsutil cors get gs://hoge-upload-test  
const handleUpload = async () => {
    if (!file) return;

    axios.defaults.headers.put['Access-Control-Allow-Origin'] = '*';

    console.log('file:', file?.name);
    console.log('fileType:', file?.type);
    const idToken = await getIdToken();
    console.log('idToken:', idToken);
    const uploadUrl = await axios.get('/api/file_upload_url', {
      headers: {
        Authorization: `Bearer ${idToken}`,
      },
      params: {
        file_name: file.name,
        content_type: file.type,
      },
    }).then((response) => {
      return response.data.signed_file_upload_url;
    });
    console.log('uploadUrl:', uploadUrl);

    const res = await axios.put(uploadUrl, file, {
      headers: {
        'Content-Type': file.type,
      },
    });

    console.log('upload response:', res);
  };

4-3. CORSエラーにはまったことと心得

はまったこと

CORSエラーが解決したのに、GCS403エラーが返された場合、chromeはなぜかCORSエラーログが出力されるので、正しく導かれない。

心得

Chromeのdevtoolsでエラーレスポンスを確認できない場合、Firefoxを使ってエラーレスポンスを確認することが大事。

参考記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?