Amazon S3 の署名付き URL を利用して Android から画像をアップロードする

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

Amazon S3 の Pre-Signed URL (署名付き URL) を利用すれば、S3 上のオブジェクトへの限定的なアクセスを提供することが可能です。
この Pre-Signed URL はオブジェクトの取得 (GET) だけでなく、アップロード (PUT) にも利用できます。この仕組みを使えば、モバイルアプリに AWS の認証情報をもたせたり、Amazon Cognito を使ったりせずとも直接 S3 へファイルをアップロードすることができます。

今回は、Scala プログラムで S3 Pre-Signed URL を発行し、その Pre-Signed URL を用いて Android アプリから画像をアップロードしてみます。

目次

S3 の Pre-Signed URL を発行する

AWS のドキュメントにある、Java の例を参考にします。以下は、Scala から AWS Java SDK を利用して S3 の Pre-Signed URL を発行するサンプルコードです。

ProfileCredentialsProvider が AWS クレデンシャルを取得できる前提のコードです。

import java.net.URL
import java.time.{ZoneId, LocalDateTime}

import com.amazonaws.HttpMethod
import com.amazonaws.auth.profile.ProfileCredentialsProvider
import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest
import com.amazonaws.services.s3.{AmazonS3Client, AmazonS3}

object S3PresignedUrlSample {

  def generatePresignedUrl(bucketName: String, objectKey: String): URL = {
    val s3Client: AmazonS3 = new AmazonS3Client(new ProfileCredentialsProvider)
    val expirationDate = LocalDateTime.now.plusHours(1)

    val generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectKey)
    generatePresignedUrlRequest.setMethod(HttpMethod.PUT)
    generatePresignedUrlRequest.setExpiration(
      java.util.Date.from(expirationDate.atZone(ZoneId.systemDefault).toInstant)
    )

    // 重要
    generatePresignedUrlRequest.setContentType("image/jpeg")

    s3Client.generatePresignedUrl(generatePresignedUrlRequest)
  }

  def main(args: Array[String]): Unit = {
    val url = generatePresignedUrl("my-test", "somewhat.jpg")
    println(url.toString)
  }
}

このコードを実行すると、次のような URL が取得できます。この URL は バケット my-testsomewhat.png オブジェクトを PUT するためにのみ利用できるものです。また、URL の有効期限は現在時刻より1時間に設定されています。

https://my-test.s3.amazonaws.com/somewhat.jpg?AWSAccessKeyId=SOMEACCESSKEYID&Expires=XXXXXXXX&Signature=SOMESIGNATURE

この URL を使って、モバイルアプリから Amazon S3 に認証情報なしでオブジェクトのアップロードを行うことができます。

コード例のコメントに「重要」と書いてある部分で、受け入れる Content-Type の指定を行っています。この指定は、先に上げた AWS ドキュメントのコード例には含まれていませんが、署名付き URL を利用して Android からアップロードを行いたい場合には必須のため注意してください。

Android から画像をアップロード

発行した署名付き URL を利用して、Android からアップロード画像をアップロードします。以下に android.graphics.Bitmap を受け取って S3 へのアップロードを行う関数の例を示します。この関数は HTTP 通信を行うため、Android メインスレッドとは別のスレッドで実行する必要があります。

// import android.graphics.Bitmap

static final String PRE_SIGNED_URL = "https://my-test.s3.amazonaws.com/somewhat.jpg?AWSAccessKeyId=SOMEACCESSKEYID&Expires=XXXXXXXX&Signature=SOMESIGNATURE"

/**
 * @param bitmap アップロードする画像のBitmap
 * @return HTTPステータスコード
 */
int uploadImage(Bitmap bitmap) throws IOException {
    URL url = new URL(PRE_SIGNED_URL);
    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
    conn.setRequestMethod("PUT");
    conn.setInstanceFollowRedirects(false);
    conn.setDoOutput(true);

    // 重要
    conn.setRequestProperty("Content-Type", "image/jpeg");

    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos);
    bitmap.recycle();

    OutputStream out = new BufferedOutputStream(conn.getOutputStream());
    out.write(bos.toByteArray());
    out.close();

    conn.connect();
    return conn.getResponseCode();
}

上の例を確認すると、Android にアプリに認証情報をバンドルせずに Amazon S3 へ画像をアップロードすることが可能だとわかります。 例えば、先ほどの Scala プログラムをサーバー上で稼働させておけば、Android 側は署名付き URL をサーバーから取得して画像をアップロードをすればよいことになります。その場合には、上の例における PRE_SIGNED_URL 定数がサーバーから動的に取得する署名付き URL に置き換わります。

また、ソースコードのコメントに「重要」と書いてある部分で、 Content-Type ヘッダの設定を行っています。ここで設定する内容は、先ほど署名付き URL を発行した Scala プログラムで指定した内容と一致させなければなりません。

実のところ、上記の Content-Type 設定は iOS (Objective-C) では省略可能です。iOS のみをモバイルクライアントとして想定するのであれば、署名付き URL 発行時に Content-Type を指定せず、かつアップロードのリクエストにも Content-Type ヘッダを含めない方法も利用できます。ただし個人の意見としては、どのような場合においても、将来的な Android 対応を鑑み Content-Type ヘッダを指定する方法に寄せることを強くおすすめします。

まとめ

Amazon S3 の署名付き URL を利用して、Android から画像をアップロードできます。この方法を利用すれば、モバイルアプリに AWS の認証情報を持たせる必要はありません。また、Amazon Cognito や AWS STS を利用する必要もないため、 モバイルアプリのドメイン(関心事)から AWS の知識を取り除く ことが可能です。

参考