こんにちは、ウォンテッドリーでバックエンドエンジニアをしている平岡です 。
サーバーを経由せずクライアントからS3へ直接ファイルをアップロードする技術は、より良いユーザ体験を作るための選択肢として昨今のWebアプリケーション開発では欠かせないものとなっています。
本記事では、それらを実現する技術として、AWS S3のPresigned URLとPOST Policyについて調べたことをまとめてみたいと思います。
Presigned URL
Presigned URLは、S3オブジェクトに対する一時的なアクセス権限付きのURLを発行する仕組みです。
- サーバー側の役割:
AWS SDK(例:Pythonのboto3やNode.jsのAWS SDK)を使用し、有効期限付きのPUTリクエスト用URLを生成します。
url = s3_client.generate_presigned_url(
ClientMethod='put_object',
Params={'Bucket': 'your-bucket-name', 'Key': 'path/to/file.txt'},
ExpiresIn=3600
)
- クライアント側の役割:
サーバーから受け取ったURLに対して、ファイルを直接PUTリクエストで送信します。認証情報はURLに含まれているため、クライアント側で別途認証ヘッダーを用意する必要はありません。
const file = document.getElementById('file-input').files[0];
fetch(presignedUrl, {
method: 'PUT',
body: file,
headers: { 'Content-Type': file.type }
})
POST Policy
POST Policyは、アップロードを許可する条件(Policy Document)と署名を生成し、クライアントに渡します。クライアントはこの情報を使ってS3のエンドポイントへHTMLフォームを送信します。
- サーバー側の役割:
アップロードのファイルサイズ、ファイル名(プレフィックス)、有効期限などの条件を定義したポリシーを生成し、そのポリシーに対して署名を行います。
require 'openssl'
require 'time'
AWS_ACCESS_KEY_ID = 'AKIAIOSFODNN7EXAMPLE'
AWS_SECRET_ACCESS_KEY = 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY'
AWS_REGION = 'ap-northeast-1'
BUCKET_NAME = 'your-bucket-name'
KEY_PREFIX = 'uploads/'
EXPIRATION_TIME = (Time.now.utc + 3600).iso8601.gsub(/\.\d+Z/, 'Z')
policy_document = {
'expiration' => EXPIRATION_TIME,
'conditions' => [
{'bucket' => BUCKET_NAME},
['starts-with', '$key', KEY_PREFIX],
{'acl' => 'public-read'},
{'success_action_status' => '201'},
{'x-amz-credential' => "#{AWS_ACCESS_KEY_ID}/#{Date.today.strftime('%Y%m%d')}/#{AWS_REGION}/s3/aws4_request"},
{'x-amz-algorithm' => 'AWS4-HMAC-SHA256'},
{'x-amz-date' => Time.now.utc.strftime('%Y%m%dT%H%M%SZ')}
]
}
policy_json = policy_document.to_json
base64_policy = Base64.strict_encode64(policy_json)
puts "Base64 Policy: #{base64_policy}"
def get_signature_key(key, date_stamp, region_name, service_name)
k_date = OpenSSL::HMAC.digest('sha256', "AWS4#{key}", date_stamp)
k_region = OpenSSL::HMAC.digest('sha256', k_date, region_name)
k_service = OpenSSL::HMAC.digest('sha256', k_region, service_name)
k_signing = OpenSSL::HMAC.digest('sha256', k_service, 'aws4_request')
k_signing
end
date_stamp = Date.today.strftime('%Y%m%d')
signature_key = get_signature_key(AWS_SECRET_ACCESS_KEY, date_stamp, AWS_REGION, 's3')
signature = OpenSSL::HMAC.hexdigest('sha256', signature_key, base64_policy)
puts "---"
puts "AWS Access Key ID: #{AWS_ACCESS_KEY_ID}"
puts "x-amz-credential: #{AWS_ACCESS_KEY_ID}/#{date_stamp}/#{AWS_REGION}/s3/aws4_request"
puts "x-amz-date: #{Time.now.utc.strftime('%Y%m%dT%H%M%SZ')}"
puts "Policy (Base64): #{base64_policy}"
puts "**Signature (x-amz-signature):** #{signature}"
- クライアント側の役割:
S3のエンドポイントをactionに指定したHTMLフォームを作成し、サーバーから受け取ったポリシー情報や署名をhiddenフィールドとして埋め込みます。ユーザーがファイルを選択しフォームを送信すると、S3に直接POSTリクエストが送られます。
form action="https://.s3.amazonaws.com/" method="POST" enctype="multipart/form-data">
input type="hidden" name="key" value="${filename}" />
input type="hidden" name="policy" value="" />
input type="hidden" name="x-amz-signature" value="" /> input type="hidden" name="x-amz-credential" value="///s3/aws4_request" />
input type="hidden" name="x-amz-algorithm" value="AWS4-HMAC-SHA256" />
input type="hidden" name="x-amz-date" value="" />
input type="hidden" name="acl" value="public-read" />
input type="hidden" name="success_action_status" value="201" />
input type="file" name="file" />
input type="submit" value="Upload to S3" />
form>
CORS制約(Cross-Origin Resource Sharing)
Presigned URLは、前述の通りJavaScriptのfetchやXMLHttpRequestといったスクリプト通信(PUTリクエスト)としてS3にアクセスする仕組みです。JavaScriptでS3(異なるオリジン)にアクセスするため、CORS(Cross-Origin Resource Sharing)の設定が必要になります。
一方POST Policy に関しては、基本的に純粋なHTMLフォーム送信として使う限りは、CORSの設定を行う必要はありません。
まとめ
いかがでしたでしょうか?同じファイルアップロードを実現する技術でありながら、Presigned URLはJavaScriptでのアクセスを前提にリクエストする仕組みであり、一方Post Policyは、HTMLのformを前提にリクエストする仕組みという違いがあります。
それゆえ、セキュリティ上の制約にも違いがありその違いを理解した上で使い分けをすることが必要になります。
元の記事を確認する