HTML FormでS3にPOST

2018-02-24

#AWS

AWSのリファレンスを参考に、FormでS3バケットへのPOSTを試してみました。

準備

FormでS3にPOSTするためには、各フィールドに送信するファイルに合わせ、POST Policy、Signature、アクセスID、その他ファイルに関する情報を設定する必要があります。
(POST Policy:POSTする時につける設定情報、Signatureはそれをシークレットキーでhashしたもの)

バケットポリシー

フィールドにアクセスIDを設定しますが、このアクセスIDに対応するIAMユーザーは、POST先のバケットに対する操作権限が必要です。
基本的なAWSのIAMユーザーの考えですね。

次のようにバケットポリシーを設定します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
      "AWS": "arn:aws:iam::<12桁の数字>:user/<IAMユーザー名>"
      },
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::<バケット名>/*"
      ]
    }
  ]
}

ここでは全ての権限与えておりますが、実際に使う場合はActionやConditionブロックで制限したほうがよいかと思います。

フォーム

続いてフォーム。
multipart/form-dataで、アップロードするファイルの属性や認証情報をPOSTします。
各パラメータについてはリファレンスの[HTML Form Fields]をご参考。

<form action="http://<bucket name>.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
  <label>
    S3 Tags for File: <input type="text"  name="x-amz-meta-tag" value="" /><br />
  </label>
  <label>
    Content-Type: <span>image/png</span>
  </label>

  <!-- AWSが${filename}に選択したファイル名を入れてくれる -->
  <input type="hidden" name="key" value="from-form${filename}" />
  <input type="hidden" name="acl" value="private" />
  <input type="hidden" name="success_action_redirect" value="[任意のURL]" />
  <input type="hidden" name="x-amz-server-side-encryption" value="AES256" />
  <input type="hidden" name="Policy" value='[POST PlolicyをBase64エンコードした値]' />
  <input type="hidden" name="X-Amz-Signature" value="[Policyをもとに作成したSignature]" />
  <input type="hidden" name="X-Amz-Credential" value="/20180219/ap-northeast-1/s3/aws4_request" />
  <input type="hidden" name="X-Amz-Algorithm" value="AWS4-HMAC-SHA256" />
  <input type="hidden" name="X-Amz-Date" value="20180219T000000Z" />
  <input type="hidden" name="Content-Type" value="image/png" />
  <label>
    File: <input type="file" name="file" /> <br />
  </label>

  <!-- The elements after this will be ignored -->
  <!-- submitより後にinput要素を置いても送信できないので、submitは一番最後に書く。 -->
  <input type="submit" name="submit" value="Upload to Amazon S3" />
</form>

試していて、次のようなところで手間取りました。
ただの私の確認不足なのですが……

Signature生成

POST PolicyをBase64エンコードしたものをPolicyというFieldに指定。
それをStringToSignとして、認証情報(Signature)を生成しFieldにのせます。

生成手順を参考にPHPで実装したのですが、各処理ステップでキー文字列を16進数文字列に変換してしまい、上手く認証が通らず?になってました。
正しくは、キーをバイナリデータとしてそのまま渡せばOK。16進数文字列にするのは生成したSignatureをFormのフィールドに指定する時だけ。

function makeSignatureKey($awsSecretKey, $dateStamp, $regionName, $serviceName) {
  // 間違い
  //    $kDate = bin2hex(signHash('AWS4' . $awsSecretKey, $dateStamp));
  //    $kRegion = bin2hex(signHash($kDate, $regionName));
  //    $kService = bin2hex(signHash($kRegion, $serviceName));
  //    $kSign = bin2hex(signHash($kService, 'aws4_request'));

  $kDate = signHash('AWS4' . $awsSecretKey, $dateStamp);
  $kRegion = signHash($kDate, $regionName);
  $kService = signHash($kRegion, $serviceName);
  $kSign = signHash($kService, 'aws4_request');
  return $kSign;
}

Fieldの位置

各フィールド、POST Policyも大丈夫そうなのに、[key]フィールドが不足というエラーが発生。
原因は、コードをととえている時にsubmitの位置を上にしてしまい、それ以降に書いているパラメータを送れていないからでした。

<form action="http://<bucket name>.s3.amazonaws.com/" method="post" enctype="multipart/form-data">
  <!-- The elements after this will be ignored -->
  <input type="submit" name="submit" value="Upload to Amazon S3" />

  <!-- submit以降のフィールドが無視されるため、パラメータ不足エラーとなる。 -->

  <label>
    S3 Tags for File: <input type="text"  name="x-amz-meta-tag" value="" /><br />
  </label>
  <label>
    Content-Type: <span>image/png</span>
  </label>
  <input type="hidden" name="key" value="from-form${filename}" />
  ...
</form>

リファレンスを見なおすと、<!-- The elements after this will be ignored -->とちゃんと書いてありますね。恥ずかしい。

サンプルコードはこちら

単純なPOSTだとPOST Policyのフォームが楽そうですが、少し込み入った処理(容量が大きいファイル送信など)をする場合は、JavaScriptのSDKを使ったほうがよさそうです。
また試してみよう。

© kdtakahiro 2018