Tech Blog

グローバルな家族アプリFammを運営するTimers inc (タイマーズ) の公式Tech Blogです。弊社のエンジニアリングを支える記事を随時公開。エンジニア絶賛採用中!→ https://timers-inc.com/engineering

AWS S3 署名付きURL(presigned-url)を使って ブラウザから直接S3にアップロードする

今月誕生日を迎えたいわむ(@k_iwamu)です!徐々に1年経つのが早く感じます。。。
また1つ年を取りましたが、年齢だけじゃなくきちんとスキルセットも積み重ねられるよう引き続き頑張ります! (Amazonほしい物リストをブログに追加しようと思ったのですが、社内から野次が飛んできそうなので控えます。)

さて、今回はS3の署名つきURL(presigned-url)を使ってブラウザから直接S3に画像をアップロードする仕組みを作ったので、紹介したいと思います!

今回利用することになった理由

弊社でフロントエンドとバックエンド(API)を切り離して構成されているwebサービスがあります。
そのwebサービスで画像アップロード機能を実装することになりました。

画像アップロード機能では、ブラウザから直接アップロードするか、バックエンドを通じてアップロードするかで迷うことが多いと思います。
今回は、バックエンドを通じてアップロードすると、サーバーで画像を受け取ってアップロードするので処理も重くなってしまいますし、責務分離の観点から見ても避けたいということを考慮し、 ブラウザから直接アップロードすることにしました。

しかし、どのようにしてブラウザにS3への権限を与えるのかという課題がありました。
もちろん単純にAWSのKEYをブラウザで利用すると、簡単にKEYを参照できてしまい、セキュリティ面で欠陥が生じてしまいます。

そこで、署名つきURLをバックエンド(API)側で発行し、そのURLを用いてブラウザから直接アップロードさせるようにしました。

署名つきURLって何?

簡単に言うと、「S3の指定のオブジェクトへの一時的なアクセス許可」を目的として発行するURLです。
このURLを利用してアクセス制限されているオブジェクトの取得や更新を行うことができます。
URLの有効期限も決められることができるので、そのURLをずっと管理しておく必要もなくセキュリティ的にも安全です。

そういった特徴から、以下のような場面で利用することができます。

(例)

  • 通常は非公開だが、セミナー参加者だけがファイルをダウンロードできるようにしたい場合
  • 指定されたユーザーのみファイルをアップロードできるようにしたい場合

署名つきURLの構成

署名つきURLの構成は通常以下のようになっています。(デコードしたものを表示しています)

https://<bucket名>.s3.amazonaws.com/<S3のpath>
        ?Content-Type=image/jpeg
        &X-Amz-Algorithm=AWS4-HMAC-SHA256 
        &X-Amz-Credential=accesskeysample/20191125/ap-northeast-1/s3/aws4_request
        &X-Amz-Date=20191125T101156Z
        &X-Amz-Expires=60
        &X-Amz-Signature=3c6cd643b06f0eabd92db2847b0e16cf18ffi3fjj41b79a6339a40693c6e5e5a
        &X-Amz-SignedHeaders=host;x-amz-acl
        &x-amz-acl=public-read
parameter 意味
X-Amz-Algorithm 署名の計算に使用したアルゴリズム
X-Amz-Credential 署名の有効なスコープの定義 "<your-access-key-id>//<AWS-region>/<AWS-service>/aws4_request"
X-Amz-Date 署名の発行をリクエストした時間 "yyyyMMddTHHmmssZ"
X-Amz-Expires 制限時間(秒数)
X-Amz-Signature 署名(計算方法)
X-Amz-SignedHeaders 署名の計算に使用したヘッダーのリスト
x-amz-acl S3オブジェクトの公開・非公開設定

実装

画像アップロードのフロー

presigned_url_flow
presigned_url_flow

バックエンド(API)側の実装

認証

署名付きURLを発行する前に、ユーザーの認証を行っています。 署名付きURLを用いると、S3の指定のpathのみとはいえ誰でも画像をアップロードできてしまいます。
そのため、認証を行ったり、IP制限などでリクエスト自体を制限することをお勧めします。

署名付きURLの発行

次に、aws-sdkを使って署名付きURLを発行します。 S3オブジェクトのgetSignedUrlを使って署名付きURLを発行します。
バックエンド側では、S3のオブジェクトを更新できるIAMユーザーのKEYをセットしておきましょう。

import { S3 } from "aws-sdk";

/**
 * メールを送信する
 * @param bucket S3のbucket名
 * @param objectKey S3のkey名
 * @param contentType オブジェクトのコンテンツタイプ
 */
export const createPresignedUrl = (bucket: string, objectKey: string, contentType: string): string => {
  const s3 = new S3({ region: "ap-northeast-1", signatureVersion: 'v4',});
  return s3.getSignedUrl('putObject', {
    ContentType: contentType,
    Bucket: bucket,
    Key: objectKey,
    // 最小で1、最大で604800(7日間)まで設定できます。
    Expires: 60,
    // アップロードされたものをパブリックに公開するなら必須です。
    ACL: "public-read",
  });
};

CORS

S3のバケットにCORSの設定をして、ブラウザのホストから署名付きURLへのリクエストを許可する必要があります。
バケットの「アクセス権限」 => 「CORSの設定」から設定ができます。

[cors>

<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
    <AllowedOrigin>*</AllowedOrigin>
    <AllowedMethod>HEAD</AllowedMethod>
    <AllowedMethod>GET</AllowedMethod>
    <AllowedMethod>PUT</AllowedMethod>
    <AllowedMethod>POST</AllowedMethod>
    <AllowedMethod>DELETE</AllowedMethod>
    <AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
import { S3 } from "aws-sdk";

/**
 * メールを送信する
 * @param bucket S3のbucket名
 * @param objectKey S3のkey名
 * @param contentType オブジェクトのコンテンツタイプ
 */
export const createPresignedUrl = (bucket: string, objectKey: string, contentType: string): string => {
  const s3 = new S3({ region: "ap-northeast-1", signatureVersion: 'v4',});
  return s3.getSignedUrl('putObject', {
    ContentType: contentType,
    Bucket: bucket,
    Key: objectKey,
    // 最小で1、最大で604800(7日間)まで設定できます。
    Expires: 60,
    // アップロードされたものをパブリックに公開するなら必須です。
    ACL: "public-read",
  });
};

フロントエンド側の実装

画像のアップロード

フロントエンド側はaws-sdkを使わなくても、署名付きURLにPUTでリクエストすれば画像ファイルをアップロードできます。

axios({
    method: 'PUT',
    url: <presigned-url>,
    headers: {
            'Content-Type': 'image/jpeg',
        },
        data: fileData
    })

これでアップロードは完了です。
アップロード完了時のレスポンスには、アップロード先のpathは返ってきませんが、 署名付きURLで指定されたbucketのpathにアップロードされているので確認することができます。

以上の手順でブラウザ側に一時的にS3の権限を与えて画像を直接アップロードすることができます。

終わりに

画像のアップロードはいろんなサービスで実装される機能ですが、
特にクライアントとAPIサーバーが分離しているサービスではS3の権限の与え方に悩むことは多いと思います。

他にもlambdaやcloudfrontを用いて実装するなど、方法はいくつかありますが、
今回紹介した方法はその中でも比較的実装しやすい点が長所だと思いますので、ぜひ活用してみてください!

積極採用中!!

子育て家族アプリFammを運営するTimers inc.では、現在エンジニアを積極採用中!
急成長中のサービスの技術の話を少しでも聞いてみたい方、スタートアップで働きたい方など、是非お気軽にご連絡ください!
採用HP: http://timers-inc.com/engineerings

Timersでは各職種を積極採用中!

急成長スタートアップで、最高のものづくりをしよう。

募集の詳細をみる