【PHP/AWS】Amazon S3へ直接ブラウザからアップロードする処理の署名バージョン4への移行方法

前回の記事(Amazon S3の署名バージョン2廃止に伴うAWS SDK/AWS CLIの対応方法まとめ)の通り、AWSのS3において署名バージョン2の廃止がアナウンスされています。

既存のバケットに関しては引き続き署名バージョン2がサポートされるとのことですが、署名バージョン2で実装されている、ブラウザからPOSTを利用してS3へアップロードするPHPの処理を、署名バージョン4対応とする修正方法について説明します。

署名バージョン4でのブラウザアップロードの方法についてはAWSのドキュメント(英語)が存在しますが、これを基にPHPのサンプルソースコードを作成してみました。サーバーサイドはPHPの標準関数のみで実装し、フロントサイドはjQueryを利用した処理としています。

なお、本記事では署名バージョン4への移行する内容を中心に紹介するため、登場するソースコードは簡略化し、セキュリティ対策は一切含めておりません。お使いのシステムに組み込む際はバリデーション処理など適切なセキュリティ対策を施してください。

修正前(署名バージョン2)のソースコード

今回、修正するサイトは下記のような構成を想定しています。

/
├ index.html
├ upload.js
└ signature.php

index.htmlでアップロードするファイルを選択し、upload.jsに署名を取得してS3へアップロードするフロント側の処理が書かれています。署名の作成はsignature.phpで行っており、upload.jsからajax通信によって呼び出されます。

HTML (index.html)

index.htmlはファイル選択のinputタグとアップロードした画像を表示するulタグのみの構造です。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>S3 アップロードサンプル</title>
</head>
<body>
<input type="file" id="upload" accept="image/*">
<ul id="uploads"></ul>
<script
  src="https://code.jquery.com/jquery-3.4.1.min.js"
  integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo="
  crossorigin="anonymous"></script>
<script src="./upload.js"></script>
</body>
</html>

JavaScript (upload.js)

upload.jsでは、ファイルが選択された時のイベントを検知して、signature.phpと通信してアップロードに必要な情報を取得します。レスポンスを受けたら、続けて選択されたファイルをS3へアップロードする処理をjQueryのDeferredを利用して実装しています。S3へのアップロードが完了したら、レスポンスからアップロードした画像のimgタグをリストに追加します。

$(function () {
  $('#upload').on('change', function (e) {
    var file = e.target.files[0];
    if (!file) {
      return false;
    }

    $.ajax({
      url: './signature.php',
      type: 'POST',
      data: {
        name: file.name,
        size: file.size,
        type: file.type
      },
      dataType: 'json'
    })
      .then(
        function (response) {
          // 作成した署名でブラウザからS3に直接アップロード
          var key;
          var formData = new FormData();
          for (key in response.data) {
            if (response.data.hasOwnProperty(key)) {
              formData.append(key, response.data[key]);
            }
          }
          formData.append('file', file);

          return $.ajax({
            url: response.upload_url,
            type: 'POST',
            data: formData,
            dataType: 'xml',
            processData: false,
            contentType: false
          });
        },
        function (error) {
          console.log('署名作成エラー');
          console.log(error);
        }
      )
      .then(
        function (response) {
          // アップロードした画像のURLを取得
          var url = $(response).find('Location').first().text();
          // あとはご自由に処理してください
          $('#uploads').append('
  • '); }, function (error) { console.log('アップロードエラー'); console.log(error); } ) }) });

    PHP (signature.php) ※署名バージョン2

    upload.jsから送られてきたPOSTデータをもとにS3へアップロードするためのポリシーと署名を生成します。1つ1つの内容については割愛するので、詳細はHTML フォーム (AWS 署名バージョン 2)をご覧ください。

    <?php
    
    /*****************************************************************************************
     * 【注意】                                                                              *
     *  お使いの環境で使用する際はバリデーションなど適切なセキュリティ対策を行ってください   *
     *****************************************************************************************/
    $file = $_POST;
    
    // タイムスタンプ
    $now = time();
    
    // アップロード先のバケット
    $region = '[バケットのリージョン]';
    $bucket = '[バケット名]';
    
    // APIキー
    $accessKey = '[IAMユーザー アクセスキー]';
    $secretKey = '[IAMユーザー シークレットキー]';
    
    // アップロード先のKey(パス)
    $fileKey = 'technoledge/' . $file['name'];
    
    // ポリシー作成 (配列)
    $policy = [
        // アップロード期限
        'expiration' => gmdate('Y-m-d\TH:i:s.000\Z', $now + 60),
    
        'conditions' => [
            // アップロード先のバケット
            ['bucket' => $bucket],
            // ファイルパス
            ['key' => $fileKey],
            // アップロードを許可するコンテンツタイプ
            ['Content-Type' => $file['type']],
            // アップロードを許可するファイルサイズ (下限/上限)
            ['content-length-range', $file['size'], $file['size']],
            // アップロードしたファイルのACL
            ['acl' => 'public-read'],
            // アップロード成功時のレスポンスをXMLで返すオプション
            ['success_action_status' => '201'],
        ],
    ];
    
    // ポリシー文字列
    $stringToSign = base64_encode(json_encode($policy));
    
    // 署名生成
    $signature = base64_encode(hash_hmac('sha1', $stringToSign, $secretKey, true));
    
    // POSTデータ生成
    $data = [
        'key' => $fileKey,
        'Content-Type' => $file['type'],
        'acl' => 'public-read',
        'success_action_status' => '201',
        'policy' => $stringToSign,
        'AWSAccessKeyId' => $accessKey,
        'signature' => $signature,
    ];
    
    echo json_encode([
        'upload_url' => 'https://' . $bucket . '.s3.amazonaws.com',
        'data' => $data
    ]);

    修正後(署名バージョン4)のソースコード

    今回紹介するケースでは、PHPファイルのみの修正で署名バージョン4への対応ができます。

    お使いのシステムによってはHTMLファイルやJSファイルも修正が必要な場合もあります。

    PHP (signature.php) ※署名バージョン4

    大きな変更点は、下記の3点になります。

    • ポリシーのconditionsの必須項目の項目が追加された
    • 署名生成のためのハッシュ化処理が複雑化された
    • S3アップロード時にPOST送信する項目が増えた

    基本的な処理の流れは署名バージョン2の時から変更していないので、diffツールで差分を見ると修正内容がわかりやすいと思います。

    <?php
    /*****************************************************************************************
     * 【注意】                                                                              *
     *  お使いの環境で使用する際はバリデーションなど適切なセキュリティ対策を行ってください   *
     *****************************************************************************************/
    $file = $_POST;
    
    // タイムスタンプ
    $now = time();
    
    // アップロード先のバケット
    $region = '[バケットのリージョン]';
    $bucket = '[バケット名]';
    
    // APIキー
    $accessKey = '[IAMユーザー アクセスキー]';
    $secretKey = '[IAMユーザー シークレットキー]';
    
    // アップロード先のKey(パス)
    $fileKey = 'technoledge/' . $file['name'];
    
    // ポリシー作成 (配列)
    $policy = [
        // アップロード期限
        'expiration' => gmdate('Y-m-d\TH:i:s.000\Z', $now + 60),
    
        'conditions' => [
            // アップロード先のバケット
            ['bucket' => $bucket],
            // ファイルパス
            ['key' => $fileKey],
            // アップロードを許可するコンテンツタイプ
            ['Content-Type' => $file['type']],
            // アップロードを許可するファイルサイズ (下限/上限)
            ['content-length-range', $file['size'], $file['size']],
            // アップロードしたファイルのACL
            ['acl' => 'public-read'],
            // アップロード成功時のレスポンスをXMLで返すオプション
            ['success_action_status' => '201'],
            // ハッシュ化アルゴリズム (固定) ※新規追加
            ['x-amz-algorithm' => 'AWS4-HMAC-SHA256'],
            // 許可するポリシーの種類 ※新規追加
            ['x-amz-credential' => implode('/', [$accessKey, gmdate('Ymd', $now), $region, 's3', 'aws4_request'])],
            // ポリシー生成時の日時 ※新規追加
            ['x-amz-date' => gmdate('Ymd\THis\Z', $now)],
        ],
    ];
    
    // ポリシー文字列
    $stringToSign = base64_encode(json_encode($policy));
    
    // 署名生成
    $dateKey = hash_hmac('sha256', gmdate('Ymd', $now), 'AWS4' . $secretKey, true);
    $dateRegionKey = hash_hmac('sha256', $region, $dateKey, true);
    $dateRegionServiceKey = hash_hmac('sha256', 's3', $dateRegionKey, true);
    $signingKey = hash_hmac('sha256', 'aws4_request', $dateRegionServiceKey, true);
    
    // ハッシュ化されたバイナリはBase64エンコードではなく、16進数の文字列で出力
    $signature = hash_hmac('sha256', $stringToSign, $signingKey, false);
    
    // POSTデータ生成
    $data = [
        'bucket' => $bucket, // ※新規追加
        'key' => $fileKey,
        'Content-Type' => $file['type'],
        'acl' => 'public-read',
        'success_action_status' => '201',
        'policy' => $stringToSign,
        'x-amz-credential' => implode('/', [$accessKey, gmdate('Ymd', $now), $region, 's3', 'aws4_request']), // ※AWSAccessKeyIdの代わり
        'x-amz-signature' => $signature, // ※signatureの代わり
        'x-amz-algorithm' => 'AWS4-HMAC-SHA256', // ※新規追加
        'x-amz-date' => gmdate('Ymd\THis\Z', $now), // ※新規追加
    ];
    
    echo json_encode([
        'upload_url' => 'https://' . $bucket . '.s3.amazonaws.com',
        'data' => $data
    ]);

    署名生成の処理は署名バージョン2の頃と比べて大幅に複雑になっています。AWSドキュメントに掲載されている説明画像(下図)だと複雑な実装に見えますが、実際は一度実装すれば基本的にはコピペで対応できるので見た目ほど難易度は高くないと思います。

    引用元:Authenticating Requests: Browser-Based Uploads Using POST (AWS Signature Version 4)

    一点気をつける点としては、署名バージョン2ではハッシュ化した署名のバイナリデータをBase64エンコードで文字列に変換していましたが、署名バージョン4ではバイナリデータを16進数で出力しています。PHPでは`$signature = hash_hmac('sha256', $stringToSign, $signingKey, false);`のように、hash_hmacの第4引数にfalseを入れることで16進数の文字列が取得可能です。

    まとめ

    ブラウザからPOSTを利用してS3へアップロードする方法はAWSのドキュメントがまだ日本語化されていないため、PHPerに限らず1人でも多くの方のお役に立てれば幸いです。

    既存のバケットに関しては引き続き署名バージョン2がサポートされますが、現状の実装方法にもよりますが、そこまで修正範囲が大きくなることはないと思うので、この機会に移行することをおすすめ致します。